Pular para o conteúdo

Telemetria

O runner é instrumentado com OpenTelemetry, e a telemetria é no-op por default. Desenvolvimento, teste e CI não precisam de collector nenhum.

Esta é a pegadinha: a telemetria só fica ativa quando três condições valem ao mesmo tempo.

enabled = ROOTPILOT_OTEL_ENABLED && !OTEL_SDK_DISABLED && exporter !== 'none'

E o exporter é none por default. Ou seja:

Janela do terminal
# NÃO basta: o exporter continua `none` e nada é emitido
ROOTPILOT_OTEL_ENABLED=true
# funciona
ROOTPILOT_OTEL_ENABLED=true
ROOTPILOT_OTEL_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
Variável Default Notas
ROOTPILOT_OTEL_ENABLED false Master switch. Aceita true ou 1.
ROOTPILOT_OTEL_EXPORTER none otlp, console ou none. Precisa sair de none.
OTEL_SDK_DISABLED false O kill-switch padrão do OTel. true vence o master switch.
OTEL_EXPORTER_OTLP_ENDPOINT nenhum URL do collector.
OTEL_EXPORTER_OTLP_HEADERS nenhum Headers, para collector autenticado.
OTEL_EXPORTER_OTLP_PROTOCOL http/protobuf
OTEL_SERVICE_NAME rootpilot-runner Cravado na imagem do runner.
OTEL_SERVICE_VERSION versão da imagem Preenchido no build a partir da tag da release. Ausente ⇒ o atributo service.version simplesmente não é emitido.
RUNNER_INSTANCE_ID UUID por boot Vira service.instance.id — é o que separa um runner do outro quando a frota exporta pro mesmo collector.
OTEL_TRACES_SAMPLER_ARG 1 Razão de amostragem, entre 0 e 1.

O console é útil para verificar rapidamente se a instrumentação está viva sem subir collector.

Por preload, e não por chamada em código:

node --import @rootpilotsh/otel/register apps/runner/dist/index.js

O preload roda antes de o grafo de módulos da aplicação carregar, o que é o que permite à auto-instrumentação do OTel aplicar patch em http e ws primeiro. O CMD da imagem do runner já é exatamente esse comando, então você não precisa fazer nada além de setar as variáveis.

É também por isso que nome e versão do serviço vêm do ambiente: nesse ponto não existe build que os inline nem chamador em código para passá-los.

Span Quando
rootpilot.handle_invoke_batch Recebimento de um lote de invocações, sob o contexto remoto extraído do metadata da mensagem.
rootpilot.run_op Cada operação, com atributos de MCP e da operação.
rootpilot.tunnel.session Uma sessão de túnel inteira — da tentativa de conexão até o fechamento, com o resultado do handshake e por que a sessão terminou.

O último responde a pergunta que o lado do servidor não consegue: o túnel enxerga que a conexão caiu, e o motivo mora no runner. Cada sessão fecha com um rootpilot.tunnel.end_reason de vocabulário fechado — e o ponto dele é separar o que nós provocamos do que sofremos:

end_reason Significa
cert_rotated A renovação de cert trocou a identidade e forçou reconexão. Esperado, e frequente: a renovação roda a 2/3 da vida de um cert de ≤1h.
runner_stopping Drain (SIGTERM, spot reclaim). Esperado.
server_closed O control-plane fechou limpo (código 1000/1001).
network_error Queda sem frame de close (1006 e afins).
heartbeat_timeout Dois intervalos de heartbeat sem resposta — o runner derrubou e reconectou.
hello_rejected / version_incompatible O handshake foi recusado.

Sem essa separação, as reconexões por rotação de cert — dezenas por dia, por desenho — entram na mesma conta do churn que se quer investigar, e o número nunca fecha.

Métricas irmãs: rootpilot.tunnel.session.duration e rootpilot.tunnel.reconnects.total, ambas rotuladas pelo mesmo end_reason.

O contexto remoto é o detalhe que importa: ele faz o trace ser distribuído, unindo o span do control-plane ao do runner num único trace. Também há métricas por tenant.

O logger correlaciona trace_id e span_id nas linhas de log. Com a telemetria ligada, dá para sair de um span no seu backend direto para as linhas de log daquela operação.

A telemetria é drenada no SIGTERM, depois do drain do runner e independentemente de ele ter dado certo — um drain que falha não apaga a telemetria que explicaria a falha.

Ainda assim, o flush final não é a única linha de defesa: as métricas são exportadas a cada 15s, e não no default de 60s do SDK. A diferença importa para gado de vida curta — um runner que morre antes de completar um ciclo de export dependeria inteiramente do flush.

Mesmo com a telemetria desligada, o runner reporta estado ao control-plane pelo handshake:

  • accounts[]: o contexto de nuvem derivado no boot.
  • ConnectorStatus{served, health}: o que cada connector está servindo e como passou no self-check de credencial.
  • ConnectorStatus.ops: quais operações aquele connector serve.

O último existe porque uma lista plana de operações servidas não diz de quem elas são, e sem o dono, o control-plane não consegue descontar as operações de um connector com credencial quebrada.