Skip to content

English · Español

Teoría 03 — Coste y observabilidad, cosidos

Tres piezas separadas — el emisor de coste por petición de la Fase 34, la tabla CpQU de la Fase 38 y el stack Prometheus/Grafana/Tempo — se unen aquí en un único panel. La regla central: una sola fuente de verdad por número. El coste se mide en un lugar; los demás lo leen. Si dos componentes calculan lo mismo, el panel mostrará 2× lo real y nadie se dará cuenta hasta que sea tarde.

Por qué coser es difícil

Cada pieza de observabilidad, construida en aislamiento, funciona. Juntas sobre-cuentan, infra-cuentan o publican por duplicado — salvo que las fronteras se diseñen.

Las tres piezas:

  1. El emisor de coste de la Fase 34. Un middleware de FastAPI que envuelve cada petición, mide el tiempo de pared por etapa, multiplica por r_cpu (€/s en este hardware) y emite un histograma de Prometheus (lynx_cost_eur_per_request) más un atributo de span en el trace de la petición.
  2. La tabla CpQU de la Fase 38. Un batch job semanal que agrega el coste ajustado por calidad (coste ÷ accuracy ÷ peso de valor de usuario) en una tabla markdown en docs/COSTS.md. CpQU = Coste por Unidad de Calidad.
  3. El stack de observabilidad. Prometheus hace scrape de lynx_cost_eur_per_request; Grafana visualiza; Tempo almacena traces; el dashboard único infra/grafana/dashboards/capstone.json une todo.

El trabajo de la Fase 39 es el contrato entre ellos. Seis reglas concretas abajo.

Regla 1: Una sola fuente de verdad por número

El coste de una petición, en euros, existe en exactamente un sitio: el emisor de la Fase 34. Cada lector aguas abajo (panel de Grafana, agregador CpQU, log de auditoría) lee de la salida de ese emisor. Ningún lector aguas abajo recomputa el coste a partir del tiempo de pared crudo.

Por qué importa: la recomputación deriva. Si Grafana calcula duration_seconds * 0.0001 mientras el emisor calcula duration_seconds * r_cpu(t) donde r_cpu(t) conoce el nivel de hardware, los números divergen. El usuario ve "Grafana dice €0.001, el log de auditoría dice €0.0008" y la confianza colapsa.

Cumplimiento: el lab docs/COSTS.md del capstone (Fase 38 lab 03) es solo de entrada para el dashboard; el dashboard no lo invierte. El agregador CpQU lee lynx_cost_eur_per_request_total directamente desde Prometheus, nunca desde los logs de petición.

Regla 2: Atributos de span para coste joinable

Cada petición emite un span (http.request) que lleva cost_eur como atributo. Tempo almacena el span; Grafana consulta Tempo por traces y renderiza el coste por trace.

Esto evita la alternativa — emitir una métrica de Prometheus y una línea de log y un atributo de span — que triplica el almacenamiento y crea un problema de JOIN ("¿qué línea corresponde a qué trace?"). El atributo de span es el portador canónico; todo lo demás deriva.

La acción exacta del emisor por petición:

span = current_span()
span.set_attribute("cost.eur", request_cost)
span.set_attribute("cost.stage.tokenize.eur", stage_costs["tokenize"])
span.set_attribute("cost.stage.retrieve.eur", stage_costs["retrieve"])
# ... un atributo por etapa
cost_histogram.observe(request_cost)   # Prometheus, para series temporales

El histograma de Prometheus existe porque los traces se muestrean (típicamente 1%) y la serie temporal de coste debe ser exacta. Ambas vistas se pueblan desde un único cómputo.

Regla 3: Identidad de descomposición de coste

La identidad del §2 del Plan:

\[\text{cost}_{\text{req}} = \sum_{\text{stages}} \text{cost}_{\text{stage}} = \sum_{\text{stages}} T_{\text{stage}} \cdot r_{\text{cpu}}\]

El capstone verifica esto en runtime dentro de cada petición. El emisor calcula ambos lados; si difieren en más de 0.1%, la petición registra un evento cost_identity_violation y el panel parpadea en rojo.

Esto captura:

  • Una nueva etapa añadida sin registrar su timing con el emisor (su tiempo se acumula en "otros" — la identidad se mantiene solo si "otros" está acotado).
  • Una etapa que cuenta doble (su tiempo se incluye en dos bloques with timer():).
  • Una etapa que emite coste pero no aparece en la lista de etapas (coste de etapa huérfana — total < suma).

La auditoría vive en tests/integration/test_cost_identity.py y se ejecuta en cada PR.

Regla 4: Las tasas de muestreo son explícitas, no implícitas

Tres tasas de muestreo viven en la demo, y cada una está nombrada en docs/COSTS.md:

Flujo Tasa de muestreo por defecto Efecto sobre el cálculo del coste
Scrape de Prometheus 100% (cada petición) La serie temporal de coste es exacta
Ingesta de traces de Tempo 10% en CI, 100% en just demo El coste adjunto al trace es una estimación (fuera de just demo)
Trace-LLM de Langfuse 100% (cuando está presente) La estimación de coste por token es exacta cuando Langfuse está activo

El panel de Coste por petición del dashboard de Grafana lee de Prometheus (100%); el panel de Coste por etapa lee de spans de Tempo (consciente de la tasa de muestreo) con una etiqueta explícita "n peticiones muestreadas" para que el visor conozca la precisión.

Modo de fallo: un visor ve el panel por etapa y asume muestreo del 100%, luego se queja de que la suma por etapas no iguala el total. La Fase 39 lo previene etiquetando siempre la tasa de muestreo en los paneles que no son del 100%.

Regla 5: CpQU es de solo lectura en la demo

CpQU de la Fase 38 = €/(accuracy × peso-valor-usuario). Es un número batch semanal, no por petición. El dashboard de la demo renderiza el último valor de CpQU como un panel de texto estático con fuente en docs/COSTS.md. La demo no recomputa CpQU.

Por qué: CpQU requiere accuracy del eval harness de la Fase 20, que se ejecuta contra el conjunto completo de eval. Calcularlo en vivo significaría ejecutar el conjunto de eval en cada petición — absurdo. El panel estático es la forma correcta.

La trampa (§5 #6 del Plan): si la demo recomputara CpQU en vivo, el dashboard mostraría valores distintos durante just demo vs el job nocturno en régimen estacionario. Los usuarios no sabrían cuál creer.

Regla 6: Los paneles de coste se pueblan en 60 segundos

DoD §7 #4: todo panel se pueble en 60 segundos desde la primera petición. Esto es un estrés sobre la ruta de coste específicamente porque:

  • El histograma de coste necesita al menos una observación → la primera petición debe completarse.
  • El intervalo de scrape de Prometheus es 15 s por defecto → hasta 15 s de retraso.
  • El refresh de Grafana en el dashboard es 5 s.
  • El histogram-quantile necesita suficientes muestras para que el bucket no esté vacío.

La secuencia del lab 00 arranca el stack, envía una petición de calentamiento y luego espera 60 s y afirma que cada panel tiene datos. Si algún panel está en "No Data" después de 60 s, el contrato subyacente de ese panel está roto — o la etiqueta de la métrica es incorrecta, el rango de tiempo es demasiado corto, o el scrape no se disparó. El lab recorre la auditoría paso a paso.

Regla 7: El contexto de trace sobrevive a la frontera del sandbox MCP

Para el run-through de seguridad (Lab 03), el subprocess de la herramienta MCP debe heredar traceparent. Mecanismo (según la Trampa 5 del Plan):

  1. El loop del agente, antes de spawnear el subprocess, lee el trace-context W3C del span actual (cabeceras traceparent y tracestate).
  2. Los pasa como variables de entorno al subprocess.
  3. La primera acción del subprocess es re-establecer el contexto de trace: tracer.start_span(..., parent=carrier_from_env()).
  4. Los spans del subprocess aparecen en Tempo como hijos del span de petición del agente.

El panel Orphan span count del "dashboard único" rastrea spans sin padre. La invariante de la demo: orphan_spans == 0 después de cada ejecución de demo. Un conteo no-cero aflora inmediatamente en el dashboard.

El dashboard mismo

infra/grafana/dashboards/capstone.json tiene exactamente estos paneles (la imagen completa de observabilidad de la demo):

  1. RED — Rate. Peticiones por segundo. De rate(lynx_http_requests_total[1m]).
  2. RED — Errors. Tasa de error. De rate(lynx_http_requests_total{status=~"5.."}[1m]).
  3. RED — Duration. p50, p95, p99 de extremo a extremo. De histogram_quantile(0.95, lynx_http_request_duration_seconds_bucket).
  4. Latencia por etapa. Barra apilada; una fila por etapa; tasa de muestreo anotada.
  5. Presupuesto de latencia vs real. Dos barras apiladas, lado a lado, por etapa.
  6. Coste por petición. Histograma sobre la última hora, con líneas verticales en p50 / p95.
  7. Descomposición de coste. Coste por etapa como gráfico de tarta; estado del check de identidad de la regla 4.
  8. CpQU. Panel estático desde docs/COSTS.md; delta semana sobre semana.
  9. Conteo de spans huérfanos en trace. Single-stat; umbral ≤ 0.
  10. Violaciones de identidad de coste. Single-stat; umbral ≤ 0.

Diez paneles. Cada panel tiene un contrato; cada contrato es testeable. El script de auditoría del lab 00 ejecuta la query de cada panel contra Prometheus / Tempo y afirma resultados no vacíos.

Cómo conecta CpQU con la narrativa de la Fase 39

La Fase 38 calcula CpQU. La Fase 39 lo muestra pero, más importante, lo interpreta para el visor de la demo: "esta ejecución costó €0.00043 en los 90 segundos; el CpQU semanal es €0.000NNN por unidad de calidad; el visitante ve que el currículo produjo no solo un artefacto que funciona sino uno medido."

La interpretación importa porque la lección final del currículo es "los sistemas de ML se miden, no se intuyen." El único número CpQU, mostrado en el dashboard durante los últimos 5 segundos de la demo, es la firma del currículo.

Lo que esta teoría NO cubre

  • Cómo Prometheus hace scrape en realidad. Teoría de la Fase 34.
  • Cómo OpenTelemetry agrupa spans. Teoría de la Fase 34.
  • Cómo se construyen los dashboards de Grafana. El Lab 00 recorre el flujo de trabajo click-export-commit.
  • La matemática de CpQU. Teoría 04 de la Fase 38.
  • Evaluación streaming vs batch. Teoría de la Fase 20; aquí solo consumimos la accuracy de la Fase 20.
  • Modelos de coste para GPU. Fase 35; la demo es CPU.

Siguiente: theory/04-security-and-threat-model-closeout.md — qué tres filas de amenazas replica la demo y por qué.