Ir al contenido

Telemetría

El runner está instrumentado con OpenTelemetry, y la telemetría es un no-op por defecto. Desarrollo, pruebas y CI no necesitan ningún collector.

Esta es la trampa: la telemetría solo está activa cuando se cumplen tres condiciones a la vez.

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

Y el exporter es none por defecto. Es decir:

Ventana de terminal
# NOT enough: the exporter stays `none` and nothing is emitted
WHYSTACK_OTEL_ENABLED=true
# works
WHYSTACK_OTEL_ENABLED=true
WHYSTACK_OTEL_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com
Variable Por defecto Notas
WHYSTACK_OTEL_ENABLED false Master switch. Acepta true o 1.
WHYSTACK_OTEL_EXPORTER none otlp, console o none. Tiene que salir de none.
OTEL_SDK_DISABLED false El kill switch estándar de OTel. true se impone al master switch.
OTEL_EXPORTER_OTLP_ENDPOINT ninguno URL del collector.
OTEL_EXPORTER_OTLP_HEADERS ninguno Headers, para un collector autenticado.
OTEL_EXPORTER_OTLP_PROTOCOL http/protobuf
OTEL_SERVICE_NAME whystack-runner Fijado en la imagen del runner.
OTEL_SERVICE_VERSION versión de la imagen Se completa en el build a partir del tag del release. Ausente ⇒ el atributo service.version simplemente no se emite.
RUNNER_INSTANCE_ID UUID por boot Se convierte en service.instance.id: lo que distingue un runner de otro cuando la flota exporta a un collector compartido.
OTEL_TRACES_SAMPLER_ARG 1 Razón de muestreo, entre 0 y 1.

console es útil para comprobar rápido que la instrumentación está viva sin levantar un collector.

Mediante un preload, no una llamada en el código:

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

El preload corre antes de que se cargue el grafo de módulos de la aplicación, y eso es lo que permite que la autoinstrumentación de OTel aplique el patch a http y ws primero. El CMD de la imagen del runner ya es exactamente ese comando, así que no necesitas hacer nada más que definir las variables.

También es por eso que el nombre y la versión del servicio vienen del entorno: en ese punto no hay un build que los incruste ni un llamador en el código que los pase.

Span Cuándo
whystack.handle_invoke_batch Al recibir un lote de invocaciones, bajo el contexto remoto extraído de los metadatos del mensaje.
whystack.run_op Cada operación, con atributos de MCP y de la operación.
whystack.tunnel.session Una sesión de túnel completa, desde el intento de conexión hasta el cierre, con el resultado del handshake y por qué terminó la sesión.

Ese último responde lo que el lado del servidor no puede: el túnel ve que una conexión se cayó, y el motivo vive en el runner. Cada sesión se cierra con un whystack.tunnel.end_reason de vocabulario cerrado, cuyo objetivo es separar lo que nosotros causamos de lo que sufrimos:

end_reason Significado
cert_rotated La renovación del cert cambió la identidad y forzó una reconexión. Esperado, y frecuente: la renovación corre a 2/3 de la vida de un cert de ≤1h.
runner_stopping Drain (SIGTERM, spot reclaim). Esperado.
server_closed El control-plane cerró limpiamente (código 1000/1001).
network_error Caída sin close frame (1006 y similares).
heartbeat_timeout Dos intervalos de heartbeat sin respuesta. El runner cerró el socket y se reconectó.
hello_rejected / version_incompatible El handshake fue rechazado.

Sin esa separación, las reconexiones por rotación de cert (decenas por día, por diseño) caen en el mismo saco que el churn que estás investigando, y los números nunca cuadran.

Métricas hermanas: whystack.tunnel.session.duration y whystack.tunnel.reconnects.total, ambas etiquetadas con el mismo end_reason.

Estos nombres empiezan con whystack. a partir de la 1.29.0. Antes llevaban el prefijo anterior del producto, igual que los nombres de servicio y los atributos, así que la historia que cruza esa versión vive bajo los dos (Migrar desde RootPilot).

El contexto remoto es el detalle que importa: hace que el trace sea distribuido, uniendo el span del control-plane con el del runner en un único trace. También hay métricas por tenant.

El logger correlaciona trace_id y span_id en las líneas de log. Con la telemetría encendida, puedes ir de un span en tu backend directo a las líneas de log de esa operación.

La telemetría se vacía (flush) en SIGTERM, después del drain del runner e independientemente de si ese drain tuvo éxito. Un drain que falla no debería borrar la telemetría que explicaría la falla.

Aun así, el flush final no es la única línea de defensa: las métricas se exportan cada 15s, en lugar del valor por defecto de 60s del SDK. Eso importa para el cattle de vida corta: si no, un runner que muere antes de completar un ciclo de export dependería por completo del flush.

Incluso con la telemetría apagada, el runner reporta su estado al control-plane mediante el handshake:

  • accounts[]: el contexto de nube derivado en el boot.
  • ConnectorStatus{served, health}: qué está sirviendo cada connector y cómo le fue en el self-check de credenciales.
  • ConnectorStatus.ops: qué operaciones sirve ese connector.

El último existe porque una lista plana de operaciones servidas no dice de quién son, y sin el dueño, el control-plane no puede descontar las operaciones de un connector con una credencial rota.