Artefacto 6 · Monitorización · Fase 4 · la herramienta
La bitácora escribe; alguien tiene que leer. El Explorador de registros de Google Cloud Logging es la sala donde se lee todo lo que el proyecto cuenta de sí mismo — y como toda sala de lectura seria, tiene sus normas: qué hay en las estanterías, cómo se pide un libro, y cómo se interpreta lo que pone. Este libro es el manual de uso de esa sala, escrito el día que la estrenamos.
Lo primero que sorprende al entrar: la sala no guarda solo tus logs. Cloud Logging es el archivo central de todo el proyecto, y ahí desemboca todo lo que cualquier pieza de GCP tiene que contar:
| Quién escribe | Qué cuenta | Ejemplo real del proyecto |
|---|---|---|
| Tus contenedores | el stdout de tu código | las líneas de bitacora(), el «✅ Agente listo» |
| Cloud Run (la plataforma) | peticiones HTTP, arranques, escala | cada request con su código de estado, visto desde fuera |
| Cloud Build | la construcción de imágenes | el build de cada gcloud run deploy --source |
| Logs de auditoría | quién tocó qué en el proyecto | la consulta precargada que había que borrar era justo esto |
| Secret Manager | accesos a secretos | cada vez que una revisión lee APP_TOKEN_AGENTE |
Por eso ninguna consulta útil empieza «enséñamelo todo»: el todo es un océano. Leer bien es, antes que nada, filtrar bien.
El Explorador tiene cuatro zonas, y cada una responde una pregunta distinta:
Donde se escribe el filtro, en un lenguaje propio (LQL, Logging Query Language). Es la petición al bibliotecario: «tráeme solo esto». Se puede escribir a mano o construir clicando — los desplegables de recurso/gravedad y el panel de campos escriben LQL por ti en esta misma caja. Verlo aparecer ahí es la mejor forma de aprender el lenguaje.
El electrocardiograma del servicio: el tiempo en horizontal, el volumen de líneas en vertical, coloreado por gravedad. No se lee línea a línea — se lee de un vistazo: ¿hay pulso? ¿hay picos? ¿una racha naranja de errores a las 3 de la mañana? El cronograma responde «¿cuándo pasó algo raro?» antes de que sepas qué pasó. Arrastrar sobre él acota el rango de tiempo: es zoom, no decoración.
Una fila por línea de log que pasó el filtro: Gravedad (el severity del JSON convertido en icono — ⚠️ naranja los WARNING, ℹ️ azul los INFO, 🔴 los ERROR), Hora (¡ojo, en tu zona horaria! — ver capítulo 05) y Resumen (el JSON crudo, truncado). La flechita ▸ de cada fila despliega el registro completo en árbol; dentro, el tesoro es el bloque jsonPayload: tus diez campos, uno a uno.
El más infravalorado. La consola aprende de los logs que está mostrando y te ofrece sus valores como facetas clicables: «Gravedad: Advertencia 2 · Información 1», «Service name: dobe-agente»… Dos usos: como atajo (clicar añade el filtro al editor) y como resumen instantáneo — ese recuento por gravedad ya es media consulta del DoD respondida sin escribir nada.
La consulta con la que se estrenó la sala, línea a línea — con un AND implícito entre líneas (un registro debe cumplir las tres):
resource.type="cloud_run_revision" # cedazo 1: la plataforma resource.labels.service_name="dobe-agente" # cedazo 2: el servicio jsonPayload.evento="peticion" # cedazo 3: la línea — y este es TUYO
Cedazo 1 — plataforma. De todo el océano del capítulo 01, solo lo emitido por contenedores de Cloud Run. El nombre fino importa: cloud_run_revision, no «service» — quien ejecuta código y escribe logs es la revisión concreta (la 00004-fvk del libro 17), no el servicio abstracto.
Cedazo 2 — servicio. El proyecto tiene dos servicios (dobe-agente y dobe-rag); sin esta línea sus logs saldrían mezclados. Es la misma etiqueta que un día separará las consultas del copiloto de llamadas de las del agente.
Cedazo 3 — la línea, el cedazo de autor. Dentro del stdout del agente conviven la bitácora, el saludo de arranque, los logs de uvicorn… El campo evento: "peticion" lo inventó el autor de la bitácora exactamente para esto: es la marca de agua que separa las mediciones del ruido de consola. La observabilidad se diseña en la escritura para que exista en la lectura.
Cuando Cloud Logging recoge una línea de stdout, la huele. Si es JSON válido, la desmonta e indexa campo a campo bajo jsonPayload — y cada campo se vuelve consultable (jsonPayload.estado, jsonPayload.coste_usd…) y algunos hasta cambian el comportamiento: el campo severity asciende a nivel oficial del registro y pinta el icono de gravedad. Si la línea es texto normal, queda entera como textPayload: una cadena opaca donde solo se pueden buscar palabras.
# El mismo print, dos destinos:
print("✅ Agente listo.") → textPayload (prosa: solo se busca)
print(json.dumps({"evento": "peticion"...})) → jsonPayload (dato: se consulta)
Esta es la frontera del libro 15 vista desde el otro lado: en la escritura parecía una elección estética (¿prosa o JSON?); en la sala de lectura se revela como la diferencia entre buscar y consultar. Y no hubo que instalar nada para cruzarla: el contrato «si imprimes JSON, te indexo» viene de serie.
Una misma petición, dos horas: la columna Hora decía 13:30:44 y dentro del JSON, hora_utc: 11:30:44. No es un error — son dos relojes con dos oficios. La consola traduce a la zona del espectador (Madrid, UTC+2 en verano) porque los humanos leen en hora local; la bitácora guarda UTC porque los sistemas comparan en hora universal.
La regla práctica: se escribe en UTC, se lee en local — y cuando dos fuentes discrepen en dos horas exactas, antes de sospechar un bug, sospecha del reloj. (Y un matiz de ingeniería: aunque la consola ya traduce, el hora_utc propio no sobra — si un día los logs se exportan a un CSV o a otra herramienta, el campo viaja con el dato; la traducción de la consola, no.)
La lectura más fina de la primera sesión no fue un campo — fue una ausencia. Las dos líneas de los 401 no llevan coste_usd, ni llamadas_llm, ni completado. Un lector ingenuo diría «datos incompletos». El lector que diseñó la bitácora sabe que esa ausencia es un mensaje: el portero rechazó al gorrón antes de que el agente arrancara — la cartera ni se abrió.
| Lo que se ve | Lo que significa |
|---|---|
WARNING con estado: 401 y sin campos de coste | intento rechazado gratis: el token no pasó, el LLM ni se enteró |
INFO con completado: true y coste_usd | petición redonda, con su factura del taxi |
INFO con completado: false | fallo blando: el 200 dice «terminé», este campo dice «pero sin acabar la faena» (guardarraíl de turnos) |
ERROR con estado: 500 y coste_usd | fallo duro que costó dinero — el arreglo de Wald del libro 16 en acción |
| Muchos 401 seguidos en el cronograma | alguien insiste contra el portero; molesto quizá, caro no |
En una bitácora bien diseñada, hasta el campo que falta está diciendo algo. La interpretación no es leer los datos: es saber qué esperaba encontrar y notar la diferencia.
Las recetas que interrogan la bitácora del agente. Todas comparten los dos primeros cedazos (recurso + servicio); se lista solo lo que cambia. Los operadores del LQL que usan: = igual, != distinto, >= / < comparación numérica, :* «el campo existe», y AND/OR/NOT para combinar.
| Pregunta | Se añade al embudo |
|---|---|
| Solo las peticiones redondas | jsonPayload.estado=200 |
| Fallos duros (DoD) | jsonPayload.estado>=500 |
| Fallos blandos (DoD) | jsonPayload.completado=false |
| Todo lo que no fue bien | severity>=WARNING |
| Peticiones lentas (más de 15 s) | jsonPayload.latencia_ms>=15000 |
| Las que costaron dinero | jsonPayload.coste_usd:* |
| Las del futuro copiloto, aparte | jsonPayload.ruta="/preparar-llamada" |
Y la receta de interpretación general, en orden: (1) cronograma — ¿cuándo?; (2) panel de campos — ¿cuánto de cada?; (3) filtro fino — ¿cuáles exactamente?; (4) desplegar el registro — ¿qué pasó en esta?
El Explorador filtra, muestra y cuenta — pero no agrega. Puede decirte «hay 47 líneas con estado 200», no «la media de su coste_usd es $0.0021». Para las preguntas de agregación hay tres caminos, de menos a más maquinaria:
gcloud logging read + un puñado de Python — el filtro LQL viaja tal cual al comando, y la agregación se hace en local. Es como se respondió el DoD la primera vez. Para el volumen del agente (decenas de líneas), sobra.coste_usd) y se convierte en una métrica de Cloud Monitoring, con gráficas y alertas. El siguiente escalón natural si el tráfico crece.| Término | Lo que significa aquí |
|---|---|
| Logs Explorer / LQL | la sala de lectura y su lenguaje de filtros; los desplegables escriben LQL por ti |
| jsonPayload vs textPayload | dato consultable campo a campo vs prosa opaca; la frontera la decide tu print |
| severity mapping | el campo severity del JSON asciende a nivel oficial del registro (iconos, filtros) |
| facets (panel Campos) | la consola aprende los valores de tus logs y los ofrece como filtros clicables con recuento |
| log-based metrics | destilar números de las líneas y graficarlos/alertarlos en Monitoring; el escalón siguiente |
| log retention / sink | 30 días por defecto; la historia larga se exporta a propósito, no se hereda gratis |
· · ·