Operación

Seguir una reproducción por todos los servicios

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 e2e trace-proof con 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 el traceparent correcto en el sobre; el daemon abre su span hijo de ese contexto y sus logs JSON llevan el trace_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 de dec-0135, sin aprobar). Ver Lo que falta.

La idea

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í:

SaltoCómo viaja el contextoQué se ve en el backend
Navegador → BFFCabecera 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 élSpan 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-svcHTTP con traceparent = el span del BFFSpans GET /works/:id/assets y POST /sessions, hijos del span del BFF
playback-svc → catalog-svcBus 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 → daemonSocket de control spire (unix): mismo sobre, mismo campoSpan 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ídeoNo 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 y la traza

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).

Encontrar la traza de una reproducción

  1. Desde el navegador: en las herramientas de desarrollo, la petición /_serverFn/… que devuelve el pkg_… de la sesión lleva la cabecera traceparent: 00-<trace_id>-<span>-01.
  2. Desde los logs: la línea 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.
  3. En SigNoz (lab) o Tempo (visor local), busca ese 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.

El collector

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:

  1. Receivers OTLP gRPC :4317 y HTTP :4318, ambos con Authorization: Bearer obligatorio (bearertokenauth, token en el secreto otel_ingest_token). OTLP anónimo es 401 (DS-07).
  2. memory_limiter el primero: rechaza antes de gastar.
  3. 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.
  4. 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.
  5. 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).

En Coolify

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.

Visor local

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.

La prueba: trace-proof

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:

  • requeridas: el 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;
  • pendientes (con el lane que las debe): spans del daemon por OTLP y su exportador con token, 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/.

Lo que falta

  • Daemon por OTLP (lane D y pregunta P3 de 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.
  • Spans del navegador (fase 2, pregunta P2 de 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.
  • El visor local no se ha arrancado en este entorno (sin docker): el compose resuelve (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.