OpenTelemetry
Si votre agent émet déjà de l’OpenTelemetry, mcpd est un changement d’endpoint. Cette page couvre tout le chemin OTLP : les trois endpoints, l’encodage, et chaque attribut que mcpd lit.
Les trois endpoints
mcpd sert les endpoints HTTP OTLP sur le même port que son API de requête et son interface. Il n’y a pas de collecteur à faire tourner devant, pas de second conteneur, et pas de port de réception séparé.
| Endpoint | Accepte | Répond avec |
|---|---|---|
POST /v1/traces | ExportTraceServiceRequest | partialSuccess.rejectedSpans |
POST /v1/logs | ExportLogsServiceRequest | partialSuccess.rejectedLogRecords |
POST /v1/metrics | ExportMetricsServiceRequest | partialSuccess.rejectedDataPoints |
Chaque export réussi répond avec la forme partialSuccess propre
à OTLP, qui porte un décompte. C’est ainsi qu’un collecteur dit à un
émetteur qu’il a laissé tomber quelque chose, et les exporters le lisent.
Renvoyer un corps vide laisserait un émetteur annoncer une réussite qu’il
n’a pas obtenue.
Y pointer un exporter
Deux variables font le travail. Posez-les dans l’environnement du processus qui fait tourner votre agent, et rien d’autre ne change.
exporter
# L’URL de base. Votre exporter y ajoute lui-même /v1/traces.
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
# Facultatif. Envoyez du gzip si vos payloads sont volumineux.
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip
# Seulement quand mcpd tourne avec --ingest-key.
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY"
Deux détails valent d’être connus avant de déboguer un silence. D’abord,
OTEL_EXPORTER_OTLP_ENDPOINT est une URL de base, à laquelle
l’exporter ajoute lui-même le chemin du signal. Posez plutôt
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT si vous voulez donner l’URL
complète, chemin compris. Ensuite, le port 4317 est par convention le port
gRPC dans OpenTelemetry, et 4318 le port HTTP. mcpd sert le HTTP sur 4317
parce qu’il sert tout sur un seul port : un exporter laissé sur son port par
défaut émet vers 4318 et vous ne voyez rien arriver. Nommez le port
explicitement.
Les deux encodages, pour les traces
/v1/traces lit l’en-tête content-type et accepte
l’un ou l’autre encodage : OTLP/protobuf quand il contient
protobuf, OTLP/JSON sinon. Protobuf est le défaut
d’OpenTelemetry, donc un SDK non modifié pointé sur mcpd envoie des traces
qui arrivent sans aucune configuration.
/v1/logs et /v1/metrics n’acceptent que du JSON.
Un corps protobuf y est refusé par body is not valid JSON. Si
vous envoyez les trois signaux, posez le protocole une seule fois, pour tout :
json pour chaque signal
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
Quand aucun content-type n’est envoyé, mcpd suppose
application/json.
Compression et taille
Le corps d’une requête peut arriver avec content-encoding: gzip,
avec identity, ou sans en-tête d’encodage. Tout le reste est
refusé nommément : content-encoding "br" is not supported; use gzip or
send the body uncompressed. Les réponses sont compressées par
négociation de contenu, donc une grosse trace relue sur un lien lent l’est
aussi.
Deux limites s’appliquent à une même requête. La limite de corps est de
32 MiB et se mesure sur ce qui arrive, donc gzip vous achète de la marge. La
limite de lot est de 10 000 signaux, comptés après décodage à chaque porte
d’ingestion : les spans sur /v1/traces, les enregistrements sur
/v1/logs, les points de données sur /v1/metrics et
tout ce que contient la requête sur /v1/ingest. Un lot plus
grand est refusé par 413 payload_too_large, qui nomme les deux
nombres. Les deux limites sont des réglages :
--max-body-bytes et --max-batch-signals, décrits
dans Configuration.
Ce qui se passe entre l’arrivée et le stockage
Un span OTLP et un span natif suivent le même chemin. C’est tout l’intérêt d’un pipeline partagé : l’ordre ci-dessous ne peut pas diverger entre les deux portes.
-
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.
Le calcul du coût à l’ingestion est ce qui donne un chiffre de coût à un span OTLP. mcpd le tarife avec la grille sur laquelle ce serveur tourne, au moment où le span arrive, de sorte que changer vos tarifs plus tard ne peut pas réécrire en silence ce que le mois dernier a coûté. Voir Comptabilité des coûts.
Les deux conventions sémantiques
Deux jeux concurrents de noms d’attributs coexistent pour les spans d’IA :
les gen_ai.* d’OpenTelemetry et les llm.*
d’OpenInference, avec openinference.span.kind. mcpd lit les
deux, et par-dessus ses propres noms mcpd.*. Vous ne renommez
rien.
Chaque ligne ci-dessous est lue dans l’ordre, la première correspondance gagne. Un attribut qu’aucune ligne ne nomme est conservé quand même : toute la table d’attributs aplatie est stockée sur le span, donc rien de ce que vous envoyez n’est perdu faute d’avoir été mappé.
Modèle, payload et consommation
| Champ mcpd | Lu depuis, dans l’ordre |
|---|---|
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 |
Un span qui n’a aucun des trois décomptes de tokens ne porte pas d’objet
usage du tout, plutôt qu’un objet rempli de zéros. Zéro token
en entrée et un nombre inconnu de tokens en entrée sont deux faits
différents, et un rapport de coût bâti sur le premier alors qu’il s’agissait
du second est faux d’une façon que personne ne remarque.
Corrélation
Les champs de corrélation sont ce qui vous permet de demander les exécutions d’un utilisateur, ou celles d’une session. Les attributs du span sont lus d’abord, puis ceux de la ressource.
| Champ mcpd | Lu depuis, dans l’ordre |
|---|---|
project | mcpd.project sur le span, puis sur la ressource, puis le --project du daemon |
environment | mcpd.environment, deployment.environment.name de la ressource, deployment.environment de la ressource |
service_name | service.name de la ressource |
service_version | service.version de la ressource |
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 de la ressource |
run_id, resource_id, experiment_id | mcpd.run_id, mcpd.resource_id, mcpd.experiment_id |
tags | mcpd.tags, sous forme de tableau de chaînes |
Statut et erreurs
Le code de statut OTLP 1 devient ok et
2 devient error. Un statut non défini avec une
heure de fin est enregistré comme ok, parce qu’un span qui
s’est terminé sans se plaindre s’est bien terminé. Un statut non défini sans
heure de fin reste unset : le span tourne encore, et mcpd le
stocke ainsi plutôt que d’inventer une issue.
Sur un span en erreur, le message vient de status.message, puis
d’exception.message, et à défaut des deux de la chaîne
littérale unspecified error. Le type d’erreur vient
d’exception.type et la pile d’exception.stacktrace.
Un span en erreur porte toujours un message, parce qu’un « quelque chose a
échoué » sans texte n’est pas quelque chose sur quoi agir.
Type de span
Le type décide de la façon dont un span est dessiné dans la cascade. mcpd le
déduit par ordre de spécificité, et retombe sur generic, ce qui
est toujours sûr. Mal étiqueter un span est pire que renoncer à l’étiqueter.
mcpd.span.kind, quand il nomme un type que mcpd connaît.openinference.span.kind.gen_ai.operation.name.- La présence d’un attribut que seul un type de span porte.
| Valeur de l’attribut | Type 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 |
l’un de gen_ai.system, llm.model_name, llm.provider présent | model_generation |
mcp.tool.name présent | mcp_tool_call |
http.request.method ou http.method présent | http_request |
| tout le reste | generic |
Un type de span que mcpd ne reconnaît pas sous
openinference.span.kind ou
gen_ai.operation.name retombe sur generic au lieu
d’être rejeté. La liste complète des types est dans le
modèle de données.
Logs et métriques
Les enregistrements de log sont mappés par numéro de gravité, comme la
spécification le prescrit : 1 à 4 pour trace, 5 à 8 pour debug, 9 à 12 pour
info, 13 à 16 pour warn, 17 à 20 pour error, et au-delà pour fatal. Sans
numéro de gravité, un enregistrement est traité comme info. Le message vient
de body.stringValue, le nom du logger du nom du scope, et
traceId et spanId sont conservés quand ils sont
valides, ce qui est ce qui place une ligne de log à côté du span qui l’a
émise.
Pour les métriques, les sommes deviennent des compteurs et les jauges des jauges. Les histogrammes sont enregistrés par leur somme et leur décompte, avec les bornes de classes conservées : mcpd ne prétend pas pouvoir re-dériver un quantile exact depuis un histogramme pré-agrégé. Les exemplars sont conservés, et c’est ce qui permet à un pic sur un graphique d’ouvrir l’exécution qui l’a causé. La plupart des outils les jettent.
Identifiants, et traceparent W3C
mcpd reprend exactement les formes d’identifiant d’OpenTelemetry : un identifiant de trace fait 128 bits, soit 32 caractères hexadécimaux minuscules, un identifiant de span 64 bits, soit 16. Garder un format de transport identique est ce qui supprime la couche de traduction. L’identifiant tout à zéro est réservé au sens « pas de trace » et il est refusé partout où il apparaît, y compris dans les identifiants que mcpd génère lui-même.
OTLP/JSON spécifie l’hexadécimal pour les champs d’identifiant, mais plusieurs SDK émettent du base64. mcpd accepte les deux et stocke de l’hexadécimal. Un span dont les identifiants manquent, ont la mauvaise longueur ou sont tout à zéro est ignoré, et le reste du lot est tout de même écrit : un collecteur qui rejette dix mille bons spans à cause d’un mauvais est un collecteur que les gens désactivent.
Les horodatages sont des nanosecondes depuis l’epoch Unix. Ils dépassent
253, donc OTLP/JSON les envoie comme des chaînes, et mcpd les
analyse comme des chaînes. Les lire en virgule flottante perd de la
précision, ce qui se voit à des spans qui semblent démarrer à quelques
microsecondes d’écart alors que non. Un endTimeUnixNano à
0 ou absent signifie que le span n’est pas terminé.
Un en-tête traceparent s’écrit
version-traceid-parentid-flags. mcpd l’analyse selon les règles
que pose la spécification : la version ff est refusée, la
version 00 doit compter exactement quatre champs, les versions
ultérieures peuvent en porter davantage, les identifiants doivent être
valides, et le bit 0 de l’octet de drapeaux est le drapeau
d’échantillonnage. Un en-tête mal formé n’est jamais une raison de refuser
une requête. mcpd démarre une nouvelle trace à la place.
Là où cela compte : les SDK mcpd lisent un traceparent entrant
et en émettent un sur les appels sortants, donc une trace survit à une
frontière de service. Les endpoints OTLP prennent leurs identifiants dans le
payload, parce que votre exporter les a déjà mis dans les spans. Si une
trace commence dans un service que vous n’instrumentez pas avec mcpd,
transmettez son traceparent et les deux moitiés atterrissent
sous un seul identifiant de trace.
Une requête complète
Rien de ce qui précède ne demande un SDK. Voici un export complet et valide que vous pouvez coller dans un terminal :
OTLP/JSON brut
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":""}} Ce qu’OTLP ne peut pas porter
OTLP a une forme pour les spans, les logs et les métriques. Il n’en a aucune pour un score automatique, pour le pouce baissé d’une personne ou pour une note d’aspect, et aucun vocabulaire pour le détail des tokens sur lequel un fournisseur moderne facture. Deux façons de les envoyer :
-
POST /v1/ingest, l’endpoint de lot natif de mcpd, qui porte tout le modèle de données en une seule requête. -
POST /v1/api/scoresetPOST /v1/api/feedback, qui se rattachent à une trace que vous avez déjà envoyée en OTLP.
Mélanger les deux est normal et attendu. Exportez les spans avec le SDK OpenTelemetry que vous faites déjà tourner, et postez les scores depuis votre job d’évaluation. La jointure se fait sur l’identifiant de trace. Voir Scores et retours et l’API HTTP.
Avant d’exposer le port
Sans --ingest-key, quiconque peut joindre le 4317 peut y
écrire, et sans --read-key, lire chaque prompt stocké. mcpd
journalise un avertissement au démarrage quand il se lie à une adresse autre
que la boucle locale en laissant l’une des deux ouverte. Posez-les par la
configuration, puis envoyez
la clé en Authorization: Bearer, ce que fait le
OTEL_EXPORTER_OTLP_HEADERS ci-dessus.
démarrer avec des clés
mcpd --ingest-key $INGEST_KEY --read-key $READ_KEY