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.
Encenderla requiere dos variables, no una
Sección titulada «Encenderla requiere dos variables, no una»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:
# NOT enough: the exporter stays `none` and nothing is emittedWHYSTACK_OTEL_ENABLED=true
# worksWHYSTACK_OTEL_ENABLED=trueWHYSTACK_OTEL_EXPORTER=otlpOTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.comVariables
Sección titulada «Variables»| 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.
Cómo ocurre la inicialización
Sección titulada «Cómo ocurre la inicialización»Mediante un preload, no una llamada en el código:
node --import @whystack/otel/register apps/runner/dist/index.jsEl 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.
Qué está instrumentado
Sección titulada «Qué está instrumentado»| 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.
Correlación con los logs
Sección titulada «Correlación con los logs»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.
Flush al apagar
Sección titulada «Flush al apagar»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.
Observabilidad de la flota, sin OTel
Sección titulada «Observabilidad de la flota, sin OTel»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.