Aller au contenu
mcpd
aucun daemon
Source

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.

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.

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.

  1. mcpd.span.kind, quand il nomme un type que mcpd connaît.
  2. openinference.span.kind.
  3. gen_ai.operation.name.
  4. La présence d’un attribut que seul un type de span porte.
Valeur de l’attribut Type 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
l’un de gen_ai.system, llm.model_name, llm.provider présentmodel_generation
mcp.tool.name présentmcp_tool_call
http.request.method ou http.method présenthttp_request
tout le restegeneric

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