Ir al contenido
mcpd
sin daemon
Código

OpenTelemetry

Si tu agente ya emite OpenTelemetry, mcpd es un cambio de endpoint. Esta página es todo el camino OTLP: los tres endpoints, la codificación, y cada atributo que mcpd lee.

Los tres endpoints

mcpd sirve los endpoints HTTP de OTLP en el mismo puerto que su API de consulta y su interfaz. No hay ningún colector que ejecutar delante, ni un segundo contenedor, ni un puerto de recepción aparte.

Endpoint Acepta Responde con
POST /v1/traces ExportTraceServiceRequest partialSuccess.rejectedSpans
POST /v1/logs ExportLogsServiceRequest partialSuccess.rejectedLogRecords
POST /v1/metrics ExportMetricsServiceRequest partialSuccess.rejectedDataPoints

Toda exportación correcta responde con la forma partialSuccess del propio OTLP, que lleva un recuento. Así es como un colector le dice a quien envía que descartó algo, y los exportadores lo leen. Devolver un cuerpo vacío dejaría que quien envía diera por bueno un éxito que no obtuvo.

Apunta un exportador hacia él

Dos variables hacen el trabajo. Ponlas en el entorno del proceso que ejecuta tu agente, y no cambia nada más.

exportador

# The base URL. Your exporter appends /v1/traces itself.
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json

# Optional. Send gzip if your payloads are large.
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip

# Only when mcpd runs with --ingest-key.
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY"

Vale la pena conocer dos detalles antes de ponerte a depurar un silencio. Primero, OTEL_EXPORTER_OTLP_ENDPOINT es una URL base y el exportador le añade por su cuenta la ruta de la señal. Usa OTEL_EXPORTER_OTLP_TRACES_ENDPOINT si prefieres dar la URL completa, ruta incluida. Segundo, en OpenTelemetry el puerto 4317 es por convención el de gRPC y el 4318 el de HTTP. mcpd sirve HTTP en el 4317 porque lo sirve todo en un solo puerto, así que un exportador dejado en su puerto por defecto envía al 4318 y no ves llegar nada. Nombra el puerto de forma explícita.

Las dos codificaciones, para trazas

/v1/traces lee la cabecera content-type y acepta cualquiera de las dos codificaciones: OTLP/protobuf cuando contiene protobuf, y OTLP/JSON en los demás casos. Protobuf es el valor por defecto de OpenTelemetry, así que un SDK sin modificar apuntado a mcpd envía trazas que aterrizan sin ninguna configuración.

/v1/logs y /v1/metrics aceptan solo JSON. Un cuerpo protobuf ahí se rechaza como body is not valid JSON. Si vas a enviar las tres señales, fija el protocolo una vez, de forma global:

json para cada señal

export OTEL_EXPORTER_OTLP_PROTOCOL=http/json

Cuando no se envía ningún content-type, mcpd asume application/json.

Compresión y tamaño

El cuerpo de una solicitud puede llegar con content-encoding: gzip, con identity, o sin cabecera de codificación. Cualquier otra cosa se rechaza por su nombre: content-encoding "br" is not supported; use gzip or send the body uncompressed. Las respuestas se comprimen por negociación de contenido, así que una traza grande leída por un enlace lento también viaja comprimida.

A una misma solicitud se le aplican dos límites. El límite de cuerpo es de 32 MiB y se mide sobre lo que llega, así que gzip te da margen. El límite de lote es de 10,000 señales, contadas después de decodificar en todas las puertas de ingesta: spans en /v1/traces, registros en /v1/logs, puntos de datos en /v1/metrics y todo lo que venga en la solicitud en /v1/ingest. Un lote mayor se rechaza con 413 payload_too_large nombrando ambos números. Los dos límites son ajustes: --max-body-bytes y --max-batch-signals, descritos en Configuración.

Qué pasa entre llegar y quedar guardado

Un span de OTLP y un span nativo siguen el mismo camino. Esa es la razón de compartir el pipeline: el orden de abajo no puede divergir entre las dos puertas.

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.

Calcular el costo en la ingesta es la razón de que un span de OTLP llegue a tener una cifra de costo. mcpd lo valora con el libro de precios con el que este servidor está funcionando, en el momento en que llega, así que cambiar tus tarifas después no puede reescribir en silencio lo que costó el mes pasado. Consulta Contabilidad de costos.

Las dos convenciones semánticas

Hay en uso dos conjuntos rivales de nombres de atributos para los spans de IA: el gen_ai.* del propio OpenTelemetry y el llm.* de OpenInference con openinference.span.kind. mcpd lee los dos, y encima sus propios nombres mcpd.*. Tú no renombras nada.

Cada fila de abajo se lee en orden, y gana la primera coincidencia. Un atributo que ninguna fila nombra igualmente se conserva: el mapa completo de atributos aplanado se guarda en el span, así que nada de lo que envías se pierde por no estar mapeado.

Modelo, contenido y uso

Campo de mcpd Se lee de, en orden
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 sin ninguno de los tres recuentos de tokens no lleva objeto de uso en absoluto, en vez de llevar uno lleno de ceros. Cero tokens de entrada y un número desconocido de tokens de entrada son hechos distintos, y un informe de costos construido sobre el primero cuando quería decir el segundo está equivocado de una forma que nadie nota.

Correlación

Los campos de correlación son los que te permiten pedir las ejecuciones de un usuario, o las de una sesión. Primero se leen los atributos del span, y luego los del recurso.

Campo de mcpd Se lee de, en orden
project mcpd.project en el span, luego en el recurso, luego el --project del daemon
environment mcpd.environment, deployment.environment.name del recurso, deployment.environment del recurso
service_name service.name del recurso
service_version service.version del recurso
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 del recurso
run_id, resource_id, experiment_id mcpd.run_id, mcpd.resource_id, mcpd.experiment_id
tags mcpd.tags, como un array de cadenas

Estado y errores

El código de estado 1 de OTLP se convierte en ok y el 2 en error. Un estado sin fijar que tiene hora de fin se guarda como ok, porque un span que terminó sin quejarse terminó bien. Un estado sin fijar y sin hora de fin se queda en unset: el span sigue en marcha, y mcpd lo guarda así en vez de inventarle un desenlace.

En un span con error, el mensaje sale de status.message, luego de exception.message, y si faltan los dos, de la cadena literal unspecified error. El tipo de error sale de exception.type y la pila de exception.stacktrace. Un span con error siempre lleva un mensaje, porque un «algo falló» sin texto no es algo sobre lo que puedas actuar.

Tipo de span

El tipo determina cómo se dibuja un span en la cascada. mcpd lo deduce por orden de especificidad, y recae en generic, que siempre es seguro. Etiquetar mal un span es peor que renunciar a etiquetarlo.

  1. mcpd.span.kind, cuando nombra un tipo que mcpd conoce.
  2. openinference.span.kind.
  3. gen_ai.operation.name.
  4. La presencia de un atributo que solo lleva un tipo de span.
Valor del atributo Tipo en 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
cualquiera de gen_ai.system, llm.model_name, llm.provider presentemodel_generation
mcp.tool.name presentemcp_tool_call
http.request.method o http.method presentehttp_request
cualquier otra cosageneric

Un tipo de span que mcpd no reconoce bajo openinference.span.kind o gen_ai.operation.name cae a generic en lugar de rechazarse. La lista completa de tipos está en el modelo de datos.

Logs y métricas

Los registros de log se asignan por número de severidad, tal como prescribe la especificación: de 1 a 4 es trace, de 5 a 8 debug, de 9 a 12 info, de 13 a 16 warn, de 17 a 20 error, y cualquier cosa por encima es fatal. Sin número de severidad, un registro se trata como info. El mensaje sale de body.stringValue, el nombre del logger sale del nombre del scope, y traceId y spanId se conservan cuando son válidos, que es lo que pone una línea de log junto al span que la emitió.

En métricas, las sumas se convierten en contadores y los gauges en gauges. Los histogramas se guardan por su suma y su recuento, conservando los límites de los buckets: mcpd no finge que puede volver a derivar un cuantil exacto de un histograma preagregado. Los exemplars se conservan, que es lo que permite que un pico en una gráfica abra la ejecución que lo causó. La mayoría de las herramientas los descartan.

Identificadores y traceparent de W3C

mcpd usa exactamente las formas de identificador de OpenTelemetry: un id de traza tiene 128 bits escritos como 32 caracteres hexadecimales en minúscula, y uno de span 64 bits escritos como 16. Mantener idéntico el formato de cable es lo que elimina la capa de traducción. El identificador con todo ceros está reservado para significar «sin traza» y se rechaza dondequiera que aparezca, incluso en los identificadores que genera el propio mcpd.

OTLP/JSON especifica hexadecimal para los campos de identificador, pero varios SDK emiten base64. mcpd acepta los dos y guarda hexadecimal. Un span cuyos identificadores faltan, tienen la longitud equivocada o son todo ceros se omite y el resto del lote se escribe igual, porque un colector que rechaza diez mil spans buenos por uno malo es un colector que la gente apaga.

Las marcas de tiempo son nanosegundos desde la época Unix. Superan 253, así que OTLP/JSON las envía como cadenas, y mcpd las analiza como cadenas. Leerlas como coma flotante pierde precisión, y eso se ve como spans que parecen empezar con microsegundos de diferencia cuando no era así. Un endTimeUnixNano igual a 0 o ausente significa que el span no ha terminado.

Una cabecera traceparent es version-traceid-parentid-flags. mcpd la analiza con las reglas que fija la especificación: la versión ff se rechaza, la versión 00 tiene que tener exactamente cuatro campos, las versiones posteriores pueden llevar más, los identificadores tienen que validar, y el bit 0 del byte de flags es la marca de muestreo. Una cabecera mal formada nunca es motivo para rechazar una solicitud. mcpd empieza una traza nueva en su lugar.

Dónde importa esto: los SDK de mcpd leen un traceparent entrante y emiten uno en las llamadas salientes, así que una traza sobrevive a un límite de servicio. Los endpoints OTLP toman sus identificadores del cuerpo del mensaje, porque tu exportador ya los ha puesto en los spans. Si una traza empieza en un servicio que no instrumentas con mcpd, pasa su traceparent adelante y las dos mitades aterrizan bajo un mismo id de traza.

Una solicitud completa

Nada de lo anterior necesita un SDK. Esta es una exportación completa y válida que puedes pegar en una terminal:

OTLP/JSON en crudo

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":""}}

Lo que OTLP no puede llevar

OTLP tiene una forma para spans, logs y métricas. No tiene ninguna para una puntuación automática, ni para el pulgar abajo de una persona, ni para una nota de aspecto, ni vocabulario para el detalle de tokens que factura un proveedor moderno. Hay dos maneras de enviar eso:

  • POST /v1/ingest, el endpoint de lotes nativo de mcpd, que lleva el modelo de datos completo en una sola solicitud.
  • POST /v1/api/scores y POST /v1/api/feedback, que se enganchan a una traza que ya has enviado por OTLP.

Mezclar las dos cosas es normal y está previsto. Exporta los spans con el SDK de OpenTelemetry que ya ejecutas, y publica las puntuaciones desde tu trabajo de evaluación. Se unen por el id de traza. Consulta Puntuaciones y feedback y la API HTTP.

Antes de exponer el puerto

Sin --ingest-key, cualquiera que llegue al 4317 puede escribir en él, y sin --read-key, leer todos los prompts guardados. mcpd registra una advertencia al arrancar cuando se enlaza a una dirección que no es de loopback con alguna de las dos abiertas. Ponlas con la configuración, y luego envía la clave como Authorization: Bearer, que es lo que hace OTEL_EXPORTER_OTLP_HEADERS más arriba.

ejecutar con claves

mcpd --ingest-key $INGEST_KEY --read-key $READ_KEY