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.
-
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.
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.
mcpd.span.kind, cuando nombra un tipo que mcpd conoce.openinference.span.kind.gen_ai.operation.name.- La presencia de un atributo que solo lleva un tipo de span.
| Valor del atributo | Tipo en 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 |
cualquiera de gen_ai.system, llm.model_name, llm.provider presente | model_generation |
mcp.tool.name presente | mcp_tool_call |
http.request.method o http.method presente | http_request |
| cualquier otra cosa | generic |
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/scoresyPOST /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