Cómo encontrar la traza de una reproducción y saltar a los logs de cada servicio (navegador, BFF, playback-svc, catalog-svc por el bus, daemon por el socket), qué hace el collector con lo que recibe (autenticación, entorno, redacción), el visor local otel-lgtm y la prueba e2e trace-proof que lo demuestra con una reproducción real.
Especificado; parte probada. La correlación de esta página la fija
dec-0135, que está PROPOSED. Lo que ya está en la rama y comprueba la prueba e2etrace-proofcon una reproducción real: la traza cruza navegador → BFF (con span y logs propios) → playback-svc → catalog-svc (por NATS) y llega al socket del daemon con eltraceparentcorrecto en el sobre; el daemon abre su span hijo de ese contexto y sus logs JSON llevan eltrace_id; los logs de los servicios TypeScript y del BFF también; el TTFF y sus fases viajan como evento de la traza; el collector exige el token, pone el entorno y redacta. Lo que no está todavía: el daemon no exporta sus spans ni sus logs por OTLP (van a su salida estándar; su exportador sólo manda métricas, sin token y sólo a loopback) y no hay spans del navegador (fase 2 dedec-0135, sin aprobar). Ver Lo que falta.
Una reproducción es una traza W3C. Nace en el navegador cuando se abre el reproductor (el CTA «Reproducir» de la ficha lo abre y el reproductor abre la sesión en ese momento, antes de que nadie pulse play) y viaja así:
| Salto | Cómo viaja el contexto | Qué se ve en el backend |
|---|---|---|
| Navegador → BFF | Cabecera traceparent en la server fn que abre la sesión (y en el lote de métricas que lleva el TTFF) | Nada propio todavía: el span del navegador no se exporta, así que la raíz aparece como «span ausente» |
| BFF (web SSR) | Valida el traceparent (formato estricto; tracestate y baggage se tiran; sampled lo decide el BFF) y abre su span servidor BFF serverFn hijo de él | Span BFF serverFn (servicio web) hijo de la llamada del navegador; sus logs (JSON en stdout y OTLP) llevan trace_id y el span_id de ese span |
| BFF → catalog-svc/playback-svc | HTTP con traceparent = el span del BFF | Spans GET /works/:id/assets y POST /sessions, hijos del span del BFF |
| playback-svc → catalog-svc | Bus spire por NATS: el traceparent va dentro de la cabecera firmada del sobre v2 (dec-0120) | Span cliente request qry.catalog.resolvePlaybackSource y, en catalog-svc, handle qry.catalog… hijo de él |
| playback-svc → daemon | Socket de control spire (unix): mismo sobre, mismo campo | Span cliente daemon.cmd.media.openPackaging. El daemon abre daemon.cmd.media.openPackaging con parent_span_id = ese span y lo escribe como línea JSON en su salida (no por OTLP) |
| Bytes de vídeo | No llevan cabeceras de traza (r01: el camino caliente no se toca; el daemon atará los suyos a la sesión por el SCT, dec-0135 D2) | Las fases del arranque (manifest, primer segmento, primer frame) las mide el navegador y van como atributos del evento playback.ttff |
El TTFF cuenta desde la intención de reproducir, nunca el tiempo que se tarda en pulsar: hoy el
reproductor no arranca solo, así que su origen es el gesto de play sobre el reproductor
(playback.ttff.origin = gesture); lo hecho antes de ese gesto (sesión abierta, manifest cargado)
marca playback.ttff.prewarmed = true y sus fases valen 0. El origen cta (el clic en «Reproducir»
arranca la reproducción por sí mismo) existe en el colector del navegador pero ningún stage lo usa
todavía. El lote de métricas que lleva el TTFF viaja con el traceparent de la traza del
reproductor: en playback-svc, el span playback.metrics.report (hijo del span del BFF) tiene el
evento playback.ttff con playback.ms, el origen, prewarmed y las fases
playback.ttff.phase.{session,manifest,segment,frame}_ms. Las peticiones de manifest y segmentos al
daemon no llevan traza propia: lo que dura cada fase está en ese evento, medido en el navegador.
Todos los logs de los servicios TypeScript (@styx/observability) llevan trace_id y span_id
en los campos de nivel superior del LogRecord de OTLP, que es por donde SigNoz y Grafana unen
logs y trazas. Los del daemon son una línea JSON por registro en su salida (trace_id, span_id,
parent_span_id, span.name, styx.playback.session_id): hoy se cruzan con
docker logs styx-media-daemon | grep <trace_id>, no desde el backend. Las métricas no llevan ids: sus etiquetas son vocabularios cerrados
(packages/observability/metric-registry.json).
/_serverFn/… que
devuelve el pkg_… de la sesión lleva la cabecera traceparent: 00-<trace_id>-<span>-01.plan resolved with REAL daemon delivery de playback-svc trae el
sessionId (pkg_…) y el trace_id; la del BFF playback session opened (servicio web,
JSON con trace_id y span_id), lo mismo.trace_id. Verás el span del BFF
colgando de la llamada del navegador, y de él los de playback-svc y catalog-svc. Desde cualquier span, «logs de
este span/traza» abre las líneas de ese servicio con el mismo trace_id.Las demás llamadas del navegador (el token CSRF, la biblioteca) abren su propia traza: sólo la apertura de la sesión y el lote de métricas del arranque pertenecen a la de la intención.
deploy/otel-collector-config.yaml (producción: SigNoz) y deploy/otel-collector-config.dev.yaml
(visor local) tienen el mismo pipeline para trazas, logs y métricas:
:4317 y HTTP :4318, ambos con Authorization: Bearer obligatorio
(bearertokenauth, token en el secreto otel_ingest_token). OTLP anónimo es 401 (DS-07).memory_limiter el primero: rechaza antes de gastar.resource/deployment: pone deployment.environment (lab en el de producción, dev en
el local). Un collector por despliegue; lo decide el despliegue, no el SDK de cada servicio.transform/redact y redaction/values: la segunda capa de redacción (la primera está
en origen, packages/observability/src/redaction.ts, dec-0117 A8). Las dos se generan de
las reglas de origen (packages/observability/src/collector-redaction.ts,
bun scripts/gen-otel-redact.ts): la regla por clave es la misma (palabras sueltas y en
camelCase como accessToken o privateKey, pares como api key/actor id, code de
quick-connect, la IP del cliente en client.address…) y también las formas de valor (JWT/SCT,
Bearer …, tokens styx_*_…, sct=/code=/token= en una query, emails también como %40,
rutas absolutas incluidas /app y file://, query strings). transform/redact (OTTL) borra
las claves y sanea los valores string del recurso, el span, sus eventos, el log y el punto de
métrica, el nombre del span y de cada evento, status.message y el cuerpo string del log; quita
además del recurso el argv y las rutas del proceso (process.command_args,
process.executable.path…). redaction/values aplica las mismas formas a lo que OTTL no
recorre: elementos de listas, cuerpos de log estructurados y mapas anidados. Límite: los
atributos de los links de un span no los alcanza ningún procesador del collector (OTTL sólo ve
la lista entera); los cubre la capa en origen, y ningún emisor de Styx crea links.batch el último.check:deploy exige que todo pipeline de los dos ficheros lleve transform/redact y
redaction/values seguidos y en ese orden, y que los dos bloques sean exactamente los generados
(otel-redact, otel-redact-drift): vaciar o tocar una regla a mano es rojo. La prueba funcional
con el binario real de otelcol-contrib está en tools/e2e-harness/trace-proof/collector-redact.ts:
manda por OTLP spans, logs (también con cuerpo estructurado) y una métrica con un canario de cada
clase de secreto (incluidas las de la revisión de lane E: SCT opaco y code= en url.query,
claves camelCase, IP del cliente, rutas en listas y en el recurso, status.message, nombre de
evento…) y comprueba que ninguno llega al otro lado, que lo ya saneado conserva su ruta, que el
entorno se pone, que los ids de traza no se tocan y que sin Bearer es 401. Los mutantes
no-redact, no-transform y no-values salen en rojo; el canario de los links sale como KNOWN
(el límite de arriba).
Muestreo: por cabeza en cada SDK (ParentBased con OTEL_TRACES_SAMPLER_ARG, 1.0 en el lab). El
collector no muestrea ni deriva métricas de las trazas (SigNoz calcula el RED de los spans).
No hay nada que cambiar en la aplicación: el compose de Coolify monta el mismo
otel-collector-config.yaml, así que la redacción y el entorno llegan con el despliegue. El
collector sigue sin puertos publicados (sólo styx-internal y egress hacia SigNoz) y con los
mismos dos secretos de fichero (otel_ingest_token, signoz_ingestion_key). Si se despliega otro
entorno con el mismo fichero, deployment.environment dirá lab: cambia el valor en el fichero de
ese despliegue.
Hoy el daemon no tiene OTEL_* en el compose: su exportador sólo habla con un collector en
loopback y sin token. Se añade cuando lo implemente su lane de dec-0135. La web ya arranca el
mismo SDK que los servicios (apps/web/src/server/bff-telemetry.ts: OTEL_EXPORTER_OTLP_ENDPOINT,
OTEL_INGEST_TOKEN_FILE, STYX_LOG_FORMAT=json), pero los compose de deploy/ no despliegan la
web todavía.
Para ver trazas y logs sin cuenta en el lab: deploy/docker-compose.observability.dev.yml, perfil
observability-dev. Levanta grafana/otel-lgtm (Tempo, Loki, Prometheus y Grafana en un
contenedor, fijado por digest) y un collector con otel-collector-config.dev.yaml que sustituye
al de docker-compose.infra.dev.yml, en los mismos puertos del host:
cd deploy
docker compose -f docker-compose.infra.yml -f docker-compose.infra.dev.yml up -d postgres valkey nats
docker compose -f docker-compose.observability.dev.yml --profile observability-dev up -d
# servicios bare: OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:14317 + OTEL_INGEST_TOKEN_FILE=deploy/secrets/otel_ingest_token
# Grafana: http://127.0.0.1:13000 (STYX_DEV_GRAFANA_PORT)La imagen trae ya los saltos de Tempo a Loki por trace_id (tracesToLogsV2) y de Loki a Tempo
(derivedFields). Grafana entra sin login (anónimo Admin, el defecto de la imagen): por eso sólo
escucha en 127.0.0.1. Todo el estado vive en un tmpfs y se pierde al parar.
tools/e2e-harness/trace-proof/run.sh levanta una pila propia y desechable sin docker
(Postgres, Valkey y nats-server nativos con la ACL de deploy/nats, identity-svc con el OP de
prueba, el daemon Zig, catalog-svc y playback-svc con su SDK real, la web construida) y Chromium
reproduce de verdad: CTA «Reproducir» → frames → cerrar el player. Los servicios exportan a un
receptor OTLP propio (con TRACE_PROOF_OTELCOL, a través del collector real con la configuración
de producción) y el socket del daemon pasa por un espía que lee el traceparent de cada sobre.
assert-trace.ts da un veredicto por salto:
traceparent del navegador; el span del BFF hijo de él; POST /sessions
hijo del span del BFF; logs del BFF en JSON (una línea por registro) y por OTLP con el trace_id
y el span_id de ese span; span cliente por NATS y span servidor de
catalog-svc hijo de él; span cliente IPC y el sobre del socket con ese span como padre; todos los
spans de la traza enlazados; ningún log sin trace_id mientras se atendía la traza;
styx.playback.ttff recibido y su evento con origen y fases en la traza; etiquetas dentro del
registro y ningún id (tampoco con prefijo, asset_…, w-…) como etiqueta; ningún
canario real de la corrida (SCT, cookie de sesión, Bearer, email) en lo recibido ni en stdout;
y en el daemon, sus logs JSON con el trace_id y su span hijo del span IPC de playback-svc;styx.playback.session_id en los spans, catalog-svc sin ninguna línea de log en ese salto,
métricas de reproducción fuera del registro.Mutantes (TRACE_PROOF_MUTANT), cada uno rompe una propagación en el código real y debe dar rojo:
eden-no-traceparent (el BFF no reenvía), bff-no-telemetry (el BFF sin span ni logs propios),
nats-no-traceparent (el bus no sella en ninguno de los tres sitios: request, emit, emitAcked),
ipc-no-traceparent (el cliente del socket no sella), daemon-ignores-traceparent (el daemon abre
su span sin el contexto del sobre; se compila aparte) y ts-no-redact (redacción en origen
apagada, sin collector). Para que este último se note, la corrida manda además peticiones HTTP a
los servicios con secretos reales en la query (el SCT que vio el navegador, el email del usuario,
también codificado como %40). La evidencia de cada corrida queda en
docs/track/observability/evidence/trace-proof/.
dec-0135): exportar sus spans y logs, con Bearer y
host remoto. Hoy su span y sus logs están correlacionados pero sólo en su salida estándar: en
SigNoz o Tempo la traza termina en el span cliente daemon.cmd.media.openPackaging.dec-0135): hoy las fases del arranque son
atributos del evento playback.ttff, no spans.OTEL_* del daemon en los compose, cuando haya quien los lea; la web, cuando se despliegue.docker compose config) y pasa check:deploy, pero que otel-lgtm arranque con usuario no
root, rootfs de sólo lectura y seccomp propio no está comprobado.Desplegar la nightly
Runbook de la nightly privada de master en Coolify — imágenes privadas en GHCR, credencial de pull en el host, aplicación de Coolify que fija las imágenes por digest, secretos del environment coolify-nightly, verificación, rollback y logs.
Referencia
Referencia generada de styx: API HTTP, SDK, Zig, bus, protocolos y configuración.