Ir para o conteúdo
mcpd
sem daemon
Código

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.

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.

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.

  1. mcpd.span.kind, quando nomeia um tipo que o mcpd conhece.
  2. openinference.span.kind.
  3. gen_ai.operation.name.
  4. A presença de um atributo que só um tipo de span carrega.
Valor do atributo Tipo no 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
qualquer um de gen_ai.system, llm.model_name, llm.provider presentemodel_generation
mcp.tool.name presentemcp_tool_call
http.request.method ou http.method presentehttp_request
qualquer outra coisageneric

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