OpenTelemetry
Se o seu agente já emite OpenTelemetry, o mcpd é uma troca de endpoint. Esta página é o caminho OTLP inteiro: os três endpoints, a codificação, e cada atributo que o mcpd lê.
Os três endpoints
O mcpd serve os endpoints OTLP sobre HTTP na mesma porta da sua API de consulta e da sua interface. Não há coletor para rodar na frente dele, nem um segundo contêiner, nem uma porta de recepção separada.
| Endpoint | Aceita | Responde com |
|---|---|---|
POST /v1/traces | ExportTraceServiceRequest | partialSuccess.rejectedSpans |
POST /v1/logs | ExportLogsServiceRequest | partialSuccess.rejectedLogRecords |
POST /v1/metrics | ExportMetricsServiceRequest | partialSuccess.rejectedDataPoints |
Toda exportação bem-sucedida responde com o próprio formato
partialSuccess do OTLP, carregando uma contagem. É assim que um
coletor avisa a quem enviou que descartou alguma coisa, e os exportadores
leem esse campo. Devolver um corpo vazio deixaria quem enviou reportar um
sucesso que ele não teve.
Aponte um exportador para ele
Duas variáveis fazem o trabalho. Defina as duas no ambiente do processo que roda o seu agente, e nada mais muda.
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 saber de dois detalhes antes de você depurar um silêncio. Primeiro,
OTEL_EXPORTER_OTLP_ENDPOINT é um URL base e o exportador
acrescenta o caminho do sinal a ele. Defina
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT se quiser dar o URL
completo, caminho incluído. Segundo, a porta 4317 é por convenção a porta
gRPC no OpenTelemetry, e a 4318 é a de HTTP. O mcpd serve HTTP na 4317
porque serve tudo numa porta só, então um exportador deixado na porta
padrão manda para a 4318 e você não vê nada chegar. Nomeie a porta de forma
explícita.
As duas codificações, para traces
/v1/traces lê o cabeçalho content-type e aceita
qualquer uma das duas codificações: OTLP/protobuf quando ele contém
protobuf, e OTLP/JSON caso contrário. Protobuf é o padrão do
OpenTelemetry, então um SDK sem modificação nenhuma apontado para o mcpd
manda traces que chegam sem configuração alguma.
/v1/logs e /v1/metrics aceitam só JSON. Um corpo
em protobuf ali é recusado como body is not valid JSON. Se
você manda os três sinais, defina o protocolo uma vez, globalmente:
json para todo sinal
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
Quando nenhum content-type é mandado, o mcpd assume
application/json.
Compressão e tamanho
O corpo de uma requisição pode chegar com content-encoding: gzip,
com identity, ou sem cabeçalho de codificação. Qualquer outra
coisa é recusada pelo nome: content-encoding "br" is not supported;
use gzip or send the body uncompressed. As respostas são comprimidas
por negociação de conteúdo, então um trace grande lido de volta por uma
conexão lenta também vem comprimido.
Dois limites valem para uma requisição. O limite de corpo é 32 MiB e é
medido sobre o que chega, então gzip compra espaço para você. O limite de
lote é 10.000 sinais, contados depois da decodificação em toda porta de
ingestão: spans em /v1/traces, registros em
/v1/logs, pontos de dados em /v1/metrics e tudo
que vem na requisição em /v1/ingest. Um lote maior é recusado
com 413 payload_too_large nomeando os dois números. Os dois
limites são ajustáveis: --max-body-bytes e
--max-batch-signals, descritos em
Configuração.
O que acontece entre chegar e ficar guardado
Um span OTLP e um span nativo seguem o mesmo caminho. É esse o sentido de o pipeline ser compartilhado: a ordem abaixo não pode divergir entre as duas portas.
-
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.
Precificar na ingestão é o motivo de um span OTLP receber um número de custo para começo de conversa. O mcpd o precifica com a tabela de preços com que este servidor está rodando, no instante em que ele chega, então mudar as suas tarifas depois não pode reescrever em silêncio o que o mês passado custou. Veja Apuração de custos.
As duas convenções semânticas
Há dois conjuntos concorrentes de nomes de atributo em uso para spans de IA:
o gen_ai.* do próprio OpenTelemetry e o llm.* do
OpenInference, com openinference.span.kind. O mcpd lê os dois,
e por cima deles os seus próprios nomes mcpd.*. Você não
renomeia nada.
Cada linha abaixo é lida em ordem, e a primeira que casar vence. Um atributo que nenhuma linha nomeia continua guardado: o mapa de atributos inteiro, achatado, fica no span, então nada do que você manda se perde por não ter sido mapeado.
Modelo, conteúdo e uso
| Campo do mcpd | Lido de, nesta ordem |
|---|---|
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 |
Um span sem nenhuma das três contagens de token não carrega objeto de uso nenhum, em vez de carregar um cheio de zeros. Zero tokens de entrada e tokens de entrada desconhecidos são fatos diferentes, e um relatório de custo construído sobre o primeiro quando queria dizer o segundo está errado de um jeito que ninguém percebe.
Correlação
Os campos de correlação são o que deixa você pedir as execuções de um usuário, ou as de uma sessão. Os atributos do span são lidos primeiro, depois os atributos do recurso.
| Campo do mcpd | Lido de, nesta ordem |
|---|---|
project | mcpd.project no span, depois no recurso, depois o --project do daemon |
environment | mcpd.environment, deployment.environment.name do recurso, deployment.environment do recurso |
service_name | service.name do recurso |
service_version | service.version do 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 do recurso |
run_id, resource_id, experiment_id | mcpd.run_id, mcpd.resource_id, mcpd.experiment_id |
tags | mcpd.tags, como um array de strings |
Status e erros
O código de status 1 do OTLP vira ok e o
2 vira error. Um status não definido com hora de
término é registrado como ok, porque um span que terminou sem
reclamar terminou bem. Um status não definido sem hora de término continua
unset: o span ainda está rodando, e o mcpd o guarda assim em
vez de inventar um desfecho.
Num span com erro, a mensagem vem de status.message, depois de
exception.message, e faltando as duas vem a string literal
unspecified error. O tipo do erro vem de
exception.type e a pilha de exception.stacktrace.
Um span com erro sempre carrega uma mensagem, porque "algo falhou" sem texto
nenhum não é algo sobre o que você possa agir.
Tipo de span
O tipo determina como um span é desenhado na cascata. O mcpd o deduz em
ordem de especificidade, e recorre a generic, que é sempre
seguro. Rotular um span errado é pior do que recusar-se a rotulá-lo.
mcpd.span.kind, quando nomeia um tipo que o mcpd conhece.openinference.span.kind.gen_ai.operation.name.- A presença de um atributo que só um tipo de span carrega.
| Valor do atributo | Tipo no 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 |
qualquer um de gen_ai.system, llm.model_name, llm.provider presente | model_generation |
mcp.tool.name presente | mcp_tool_call |
http.request.method ou http.method presente | http_request |
| qualquer outra coisa | generic |
Um tipo de span que o mcpd não reconhece em
openinference.span.kind ou em
gen_ai.operation.name cai para generic em vez de
ser rejeitado. A lista completa de tipos está no
modelo de dados.
Logs e métricas
Os registros de log são mapeados pelo número de severidade, do jeito que a
especificação manda: 1 a 4 é trace, 5 a 8 debug, 9 a 12 info, 13 a 16 warn,
17 a 20 error, e qualquer coisa acima disso é fatal. Sem número de
severidade, um registro é tratado como info. A mensagem vem de
body.stringValue, o nome do logger vem do nome do escopo, e
traceId e spanId são preservados quando são
válidos, que é o que põe uma linha de log ao lado do span que a emitiu.
Nas métricas, somas viram contadores e gauges continuam gauges. Os histogramas são registrados pela sua soma e pela sua contagem, com os limites dos baldes preservados: o mcpd não finge que consegue re-derivar um quantil exato de um histograma já agregado. Os exemplars são preservados, e é isso que deixa um pico num gráfico abrir a execução que o causou. A maioria das ferramentas os descarta.
Ids, e o traceparent do W3C
O mcpd usa exatamente os formatos de identificador do OpenTelemetry: um id de trace tem 128 bits, escritos como 32 caracteres hexadecimais minúsculos, e um id de span tem 64 bits, escritos como 16. Manter o formato de fio idêntico é o que elimina a camada de tradução. O id todo de zeros é reservado para significar "nenhum trace" e é recusado onde quer que apareça, inclusive nos ids que o próprio mcpd gera.
O OTLP/JSON especifica hexadecimal para os campos de id, mas vários SDKs emitem base64. O mcpd aceita os dois e guarda em hexadecimal. Um span cujos ids estejam faltando, tenham o tamanho errado ou sejam todos zeros é pulado, e o resto do lote continua sendo escrito, porque um coletor que rejeita dez mil spans bons por causa de um ruim é um coletor que as pessoas desligam.
Os carimbos de tempo são nanossegundos desde a época Unix. Eles passam de
253, então o OTLP/JSON os manda como strings, e o mcpd os
interpreta como strings. Lê-los como ponto flutuante perde precisão, o que
aparece como spans que parecem começar com microssegundos de diferença sem
que isso seja verdade. Um endTimeUnixNano igual a
0 ou ausente quer dizer que o span não terminou.
Um cabeçalho traceparent é
version-traceid-parentid-flags. O mcpd o interpreta com as
regras que a especificação define: a versão ff é recusada, a
versão 00 precisa ter exatamente quatro campos, versões
posteriores podem carregar mais, os ids precisam ser válidos, e o bit 0 do
byte de flags é a flag de amostragem. Um cabeçalho malformado nunca é
motivo para recusar uma requisição. Em vez disso, o mcpd começa um trace
novo.
Onde isso importa: os SDKs do mcpd leem um traceparent que
chega e emitem um nas chamadas que saem, então um trace sobrevive à
fronteira entre serviços. Os endpoints OTLP tiram os seus ids do conteúdo
da requisição, porque o seu exportador já os colocou nos spans. Se um trace
começa num serviço que você não instrumenta com o mcpd, repasse o
traceparent dele e as duas metades caem sob um id de trace só.
Uma requisição, por inteiro
Nada do que está acima precisa de um SDK. Esta é uma exportação completa e válida que você pode colar num terminal:
OTLP/JSON cru
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":""}} O que o OTLP não consegue carregar
O OTLP tem um formato para spans, logs e métricas. Não tem nenhum para uma nota de máquina, para o polegar para baixo de uma pessoa ou para uma nota de aspecto, e nenhum vocabulário para o detalhe de tokens sobre o qual um provedor moderno cobra. Há dois jeitos de mandar isso:
-
POST /v1/ingest, o endpoint de lote nativo do mcpd, que carrega o modelo de dados inteiro numa requisição só. -
POST /v1/api/scoresePOST /v1/api/feedback, que se prendem a um trace que você já mandou por OTLP.
Misturar os dois é normal e esperado. Exporte os spans com o SDK do OpenTelemetry que você já roda, e poste as notas a partir do seu job de avaliação. Eles se juntam pelo id do trace. Veja Notas e feedback e a API HTTP.
Antes de expor a porta
Sem nenhum --ingest-key definido, qualquer um que alcance a
4317 pode escrever nela, e sem --read-key, pode ler todo prompt
guardado. O mcpd registra um aviso na inicialização quando se liga a um
endereço que não é de loopback com um dos dois em aberto. Defina os dois com
a configuração, e
depois mande a chave como Authorization: Bearer, que é o que o
OTEL_EXPORTER_OTLP_HEADERS acima faz.
rodar com chaves
mcpd --ingest-key $INGEST_KEY --read-key $READ_KEY