Saltar a contenido

Lab guiado: Manejadores de error, introspección y logging en Flask


Punto de partida

Parte del zip que entregaste al final del lab a2. Antes de empezar:

  1. Activa el entorno virtual del proyecto (source .venv/bin/activate en Linux/Mac, o source .venv/Scripts/activate en Git Bash de Windows).
  2. Sitúate en la carpeta padre de expendedora/.
  3. Arranca el servidor web: python -m expendedora.presentation.app.
  4. Comprueba que todas las rutas del a2 funcionan (/productos, /seleccionar/A1, /comprar, etc.). Si alguna falla, revuélvete al a2 antes de seguir.

Trabajo en parejas

Si decides trabajar en pareja, sigue el procedimiento conductor/navegante del lab a1.


Antes de empezar: repasemos los conceptos

Manejadores globales de error
Hasta ahora cada route captura sus propias excepciones de dominio con try/except. Flask permite registrar funciones que se disparan ante errores HTTP concretos (404, 500…), capturen lo que capturen los routes. Sirve de red de seguridad para lo que se escapa.

Pista recurso
Flask mantiene internamente un mapa de todas las rutas registradas (app.url_map). Podemos consultarlo en tiempo de ejecución para, por ejemplo, ofrecer una página de ayuda que siempre esté al día.

Hook
Un hook (gancho) es un punto del flujo de un programa en el que puedes "enganchar" código propio para que se ejecute automáticamente, sin modificar el flujo original. @app.before_request es el hook que Flask llama antes de cada petición, sea la que sea.

Logging
El módulo logging de la librería estándar de Python permite registrar mensajes en un fichero, con timestamp y nivel (INFO, WARNING, ERROR…). Útil para auditar a posteriori qué ha pasado sin depender de lo que quede en la consola desde la que se lanza el servidor.


Paso 1 — Manejador de errores global app.py

Conceptos

Flask permite registrar funciones que se ejecutan cuando ocurre un error HTTP concreto. @app.errorhandler(404) registra la función que maneja todos los errores 404 de la app; @app.errorhandler(500) hace lo mismo para errores del servidor.

Objetivo

Añadir manejadores globales para los errores 404 y 500, de modo que aunque Flask genere el error sin pasar por un route, el usuario vea una página coherente.

Tarea

@app.errorhandler(404)
def no_encontrado(e):
    return (f"<h2>404 — No encontrado</h2><p>{e}</p>"
            f"<a href='/'>Volver</a>"), 404


@app.errorhandler(500)
def error_servidor(e):
    return ("<h2>500 — Error del servidor</h2>"
            "<p>Algo ha fallado. Prueba más tarde.</p>"
            "<a href='/'>Volver</a>"), 500
Pista

Los manejadores globales no sustituyen a los errores que ya gestionas dentro de una route. Si una route captura una excepción con except ... as e y devuelve return str(e), 404, Flask no llama al manejador global de 404: la propia route ya ha construido la respuesta. El manejador @app.errorhandler(404) se ejecuta cuando Flask genera un 404 automáticamente, por ejemplo al visitar una URL que no existe o cuando un converter rechaza un valor de la URL antes de entrar en la route. Para errores no capturados dentro de una route, Flask generará normalmente un 500, que puede gestionarse con @app.errorhandler(500).

También atrapa los 404 por converter rechazado

El manejador @app.errorhandler(404) se dispara también cuando un converter rechaza el valor de la URL — los casos que viste en el Paso 2 del lab a2 (/reponer/A1/-1, /reponer/A1/tres, /insertar/1…). Flask genera el 404 antes de llamar a tu route, pero el manejador lo captura igual. Pruébalo: visita /reponer/A1/-1 y comprueba que ahora sale tu página personalizada en lugar de la página genérica de Flask.

Pregunta

Accede a una URL que no existe, por ejemplo http://localhost:5000/noexiste. ¿Qué muestra ahora? ¿Qué mostraba antes de añadir el manejador? Prueba también con /reponer/A1/-1 (converter rechazando un valor).

Respuesta

Antes: la página de error por defecto de Flask (HTML genérico). Ahora: tu manejador personalizado. En ambos casos el código HTTP es 404 — solo cambia la presentación del error. El manejador trata igual los dos tipos de 404: URL inexistente y converter rechazado.

Comprobación — forzar un error 500 reproducible

Para verificar que el manejador 500 funciona, añade temporalmente este route de prueba, visítalo, comprueba que el manejador lo captura, y bórralo cuando termines:

1
2
3
@app.route('/error')
def forzar_error():
    raise RuntimeError("error forzado para probar el manejador 500")

Con debug=True verás el traceback técnico; en producción (sin debug) se mostraría tu manejador 500.

Reflexión

Respuestas
  • Los manejadores de error son routes especiales que capturan códigos HTTP globales.
  • Permiten dar una experiencia coherente al usuario en cualquier error (incluso en los que no tienen route propio).
  • No sustituyen al try/except en los routes — son una red de seguridad adicional para los errores que se "escapan".

Paso 2 — Route de ayuda con todas las rutas app.py

Conceptos

Flask mantiene un mapa de todas las rutas registradas accesible desde app.url_map. Esto es útil para depurar, para documentar la API, y para ofrecer al usuario una página índice con todo lo que la app puede hacer.

Objetivo

Crear un route /ayuda que liste todas las rutas registradas.

Tarea

@app.route('/ayuda')
def ayuda():
    lineas = ['<h2>Rutas disponibles</h2><ul>']
    for regla in app.url_map.iter_rules():
        if regla.endpoint != 'static':
            lineas.append(
                f"<li><code>{regla.rule}</code> — {regla.endpoint}</li>"
            )
    lineas.append('</ul>')
    return '\n'.join(lineas)
Pista

app.url_map.iter_rules() devuelve todas las reglas registradas. Incluye una regla especial, static, que Flask usa internamente para servir ficheros estáticos (CSS, imágenes…); la filtramos porque no es una ruta de nuestra app.

Comprobación

Visita http://localhost:5000/ayuda. Debe aparecer una lista completa con todas las rutas que has creado.

Pregunta

¿Por qué /ayuda muestra rutas como /producto/<codigo> con los placeholders <codigo> literales en vez de URLs concretas?

Respuesta

Porque url_map contiene el patrón de cada ruta, no las URLs concretas. Para generar una URL concreta haría falta pasar valores con url_for, pero iter_rules solo conoce las plantillas.

Reflexión

Respuestas
  • app.url_map es la "tabla de routing" interna de Flask, accesible en tiempo de ejecución.
  • Una página /ayuda generada automáticamente siempre está al día: si añades un route, aparece solo.
  • En labs posteriores, cuando uses templates, esta información aparecerá más bonita con Jinja2.

Paso 3 — Logging de peticiones app.py

Conceptos

Registrar cada petición en un fichero permite auditar la actividad de la app sin depender de lo que aparezca en el traceback del navegador. El módulo logging de la librería estándar de Python ya lo tienes; Flask lo integra de forma natural.

Objetivo

Configurar un log de peticiones que escriba en un fichero expendedora.log.

Tarea

Al inicio de app.py, después de los imports y antes de crear la app, añade la configuración:

1
2
3
4
5
6
7
import logging

logging.basicConfig(
    filename='expendedora.log',
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
)

Y después de crear app = Flask(__name__), añade el "gancho" que se ejecuta antes de cada petición:

1
2
3
4
5
from flask import request   # junto a los demás imports

@app.before_request
def log_peticion():
    app.logger.info(f"{request.method} {request.path}")

Comprobación

  • Visita varias URLs en el navegador.
  • En otra terminal, ejecuta tail -f expendedora.log (o abre el fichero con tu editor).
  • Cada petición debe aparecer con timestamp, nivel y método + ruta.

Pregunta

El log se escribe en expendedora.log. ¿Conviene incluir ese fichero en el zip de entrega? ¿Y en un .gitignore si fuera un repositorio Git?

Respuesta
  • En la entrega: no. Es ruido generado por las pruebas que tú mismo hiciste; no es parte del código ni del resultado del lab.
  • En .gitignore: sí. Es un fichero generado en ejecución. Los ficheros generados (logs, bases de datos de desarrollo, caches) no se versionan; solo se versiona el código que los genera.

Reflexión

Respuestas
  • @app.before_request es un "hook" que Flask llama antes de ejecutar cualquier route.
  • El log es una herramienta transversal: no pertenece a ningún route concreto pero los observa a todos.
  • En producción, el nivel del log y el destino (fichero, syslog, servicio externo) se ajustan. logging.INFO es un nivel moderadamente verboso, útil para desarrollo.

Paso 4 — Comparación final menu.py vs app.py app.py

Conceptos

Ahora que la interfaz web cubre toda la funcionalidad del menú y tiene observabilidad propia, es momento de consolidar la equivalencia entre ambas capas de presentación y verificar que la arquitectura se ha respetado.

Objetivo

Documentar explícitamente la equivalencia entre menu.py y app.py para confirmar que la web cubre exactamente las mismas operaciones que la consola.

Tarea

Añade esta tabla como comentario al inicio de app.py:

# EQUIVALENCIA menu.py <-> app.py
#
# | menu.py                                | app.py (Flask)                                 |
# |----------------------------------------|------------------------------------------------|
# | if opcion == "1": opcion_mostrar()     | @app.route('/productos')                       |
# | if opcion == "2": opcion_seleccionar() | @app.route('/seleccionar/<codigo>')            |
# | if opcion == "3": opcion_insertar()    | @app.route('/insertar/<float:cantidad>')       |
# | if opcion == "4": opcion_comprar()     | @app.route('/comprar')                         |
# | if opcion == "5": opcion_cancelar()    | @app.route('/cancelar')                        |
# | if opcion == "6": opcion_reponer()     | @app.route('/reponer/<codigo>/<int:unidades>') |
# | if opcion == "7": opcion_agregar()     | @app.route('/agregar/<codigo>/...')            |
# | if opcion == "8": opcion_agregar_desc()| @app.route('/agregar_descuento/<codigo>/...')  |
# | if opcion == "9": opcion_eliminar()    | @app.route('/eliminar/<codigo>')               |
# | if opcion == "10": opcion_buscar()     | @app.route('/buscar/<texto>')                  |
# | input("Código: ")                      | <codigo> en la URL                             |
# | print("X " + str(e))                   | return str(e), 4xx                             |
# | servicio.listar_productos()            | (igual, sin cambios)                           |
# | servicio.obtener_producto(codigo)      | (igual, sin cambios)                           |
# | ServicioExpendedora(repo)              | (igual, via crear_servicio_sqlite)             |
#
# Unico cambio en application/servicios.py: anadido el metodo saldo()
# que delega en MaquinaExpendedora.saldo (no habia forma de leerlo desde fuera).
#
# Archivos NO modificados:
# - domain/         sin cambios
# - infrastructure/ sin cambios
# - presentation/menu.py   sin cambios (sigue funcionando igual)

Pregunta

¿Qué línea de servicios.py ejecuta el route /comprar? Localízala exactamente.

Respuesta

return self._maquina.comprar() dentro del método comprar de ServicioExpendedora. Es exactamente la misma línea que ejecuta opcion_comprar(servicio) desde menu.py. La capa de aplicación no ha cambiado — solo le hemos construido una nueva puerta de entrada.

Comprobación

Abre servicios.py, maquina.py y menu.py. Confirma visualmente que:

  • maquina.py no se ha tocado.
  • servicios.py solo ha crecido en un método (saldo), y es delegación pura.
  • menu.py no se ha tocado.

Arranca el menú en una terminal (python -m expendedora.presentation.menu) y el servidor web en otra (python -m expendedora.presentation.app). Agrega un producto por la web, refresca la lista en el menú: el producto aparece. Ambas presentaciones comparten el mismo repositorio SQLite.

Reflexión

Respuestas
  • app.py y menu.py son dos presentaciones distintas del mismo sistema.
  • Coexisten: la expendedora funciona por consola y por web simultáneamente sobre la misma base de datos.
  • La arquitectura por capas ha cumplido su promesa: cambiar la presentación no rompe nada.

Paso 5 — Actualizar la documentación del proyecto

Conceptos

Un proyecto bien mantenido tiene su documentación sincronizada con el código. Igual que en el lab a1, terminamos el trabajo actualizando los ficheros que han cambiado por culpa de este lab.

Objetivo

Dejar README.md, CHANGELOG.md, docs/EJECUCION.md y .gitignore en sintonía con la nueva capa de observabilidad.

Tarea

CHANGELOG.md

Añade una entrada nueva al inicio del fichero:

## [0.7.0] - (Fase 07: observabilidad y manejadores globales)

### Added
- `presentation/app.py`: manejadores globales `@app.errorhandler(404)` y `@app.errorhandler(500)`.
- `presentation/app.py`: route `/ayuda` que lista todas las rutas registradas mediante `app.url_map`.
- `presentation/app.py`: logging de peticiones con `@app.before_request`; se genera `expendedora.log` al arrancar.
- `presentation/app.py`: tabla de equivalencia `menu.py``app.py` como comentario inicial.
- `.gitignore`: entrada `*.log` para no versionar ficheros de log.

README.md

  1. En la sección Uso (interfaz web), añade /ayuda a la lista de rutas disponibles:
- `/ayuda` — lista todas las rutas registradas.
  1. Añade al final de esa misma sección una nota:
Al arrancar el servidor se crea automáticamente `expendedora.log` en la carpeta desde la que lances el comando. Contiene una línea por cada petición HTTP recibida.

docs/EJECUCION.md

Añade al final de la sección Ejecutar interfaz web:

El fichero `expendedora.log` se genera junto al comando que lanza el servidor (nivel INFO, formato timestamp + método + ruta). No se incluye en la entrega — es ruido generado por tus pruebas.

.gitignore

Si existe, añade la línea *.log. Si no existe, créalo con este contenido:

.venv/
__pycache__/
*.log

Comprobación

Abre README.md y comprueba que alguien que no conozca el proyecto puede saber, solo leyéndolo, que ahora tiene manejadores globales, route de ayuda y logging. Arranca el servidor, visita una URL inexistente, comprueba que el expendedora.log ha registrado la petición.


Checklist de entrega

  • Manejadores globales @app.errorhandler(404) y @app.errorhandler(500) en app.py.
  • Route /ayuda con lista de rutas registradas mediante app.url_map.
  • Logging configurado con logging.basicConfig y hook @app.before_request. expendedora.log se genera al recibir peticiones.
  • Tabla de equivalencia menu.pyapp.py escrita como comentario al inicio de app.py.
  • CHANGELOG.md con entrada [0.7.0].
  • README.md actualizado con /ayuda y la nota sobre expendedora.log.
  • docs/EJECUCION.md actualizado con la nota de log.
  • .gitignore incluye *.log.
  • Ningún cambio en domain/, application/, infrastructure/ ni presentation/menu.py. Toda la observabilidad vive en presentation/app.py.
  • Entregado un zip con todo el proyecto — será la base del siguiente lab.

Problemas comunes

NameError: name 'request' is not defined

Falta from flask import request al inicio de app.py. Revisa el Paso 3.

expendedora.log no se crea

logging.basicConfig tiene que llamarse antes de que Flask arranque (por eso va al inicio del fichero, no dentro de una función). Asegúrate de que el orden es: import logginglogging.basicConfig(...)app = Flask(__name__) → hooks y routes.

El manejador 500 no se muestra (veo el traceback técnico)

Con debug=True Flask muestra su traceback interactivo en lugar de tu manejador. Es normal y útil en desarrollo. Para ver el manejador real, cambia temporalmente app.run(debug=True) por app.run(debug=False), vuelve a probar /error, y luego restaura debug=True.

/ayuda no aparece en su propia lista

Si tu /ayuda no aparece en la propia lista, asegúrate de que el @app.route('/ayuda') está registrado antes de leer app.url_map — es decir, que la declaración del decorador se ha ejecutado ya. Si pones el route en un fichero aparte que no se importa, la regla no se añade al mapa.