Comunicación con el usuario y logging para plugins de QGIS

8 minuto de lectura

Escuché hace un tiempo una noticia de cómo la comunicación fluida con los pasajeros en los aeropuertos (hora del vuelo, tiempo restante, retrasos…) mejoraba considerablemente la experiencia de estos. Tener información de lo que está pasando o va a pasar es de gran ayuda y reduce el nerviosismo, la ansiedad o la incertidumbre que puede suponer hacer un vuelo, sobre todo si no se hace con frecuencia.

Cuando programamos una aplicación o página web esta experiencia de usuario se mejora siguiendo las especificaciones funcionales marcadas en el diseño previo del aplicativo.

Puede ocurrir que el desarrollo sea técnicamente correcto, que estén definidas tanto las entradas y salidas que va a producir y que si alguno de los procesos falla existan un flujo para su correcta solución.

Cuando estamos programando la aplicación, tenemos muchas herramientas para comprobar que todo vaya de la forma deseada. Pero cuando el trabajo ya está en producción y está siendo usado por el usuario final, el caos, las llamadas y las quejas comienzan a producirse cuando el cliente no recibe la información de posibles motivos que hacen que el programa “haga lo que debe de hacer”. Todo esto conlleva una percepción negativa del producto con las consecuencias que esto puede traer.

En la siguiente entrada se va a exponer diversas herramientas que la API de QGIS podemos usar para mejorar esa comunicación con el usuario final cuando desarrollamos un complemento de QGIS.

Al programar usando PyQGIS contamos ya con un conjunto de opciones que nos van a facilitar añadir estas ayudas. Veremos estas posibilidades y las completaremos usando el módulo logging de Python orientado al registro procesos.

Requisitos funcionales de nuestro complemento de QGIS.

He usado el repositorio QGIS Minimal Plugin de Martin Dobias que contiene los archivos y configuración mínimos para hacer un complemento de QGIS.

El complemento añade un botón denominado “Check!” a la barra de QGIS que realiza los siguientes procesos.

  • (P01) Localiza la capa llamada points en nuestro proyecto de QGIS.
  • (P02) Realizar una conexión a una base de datos PostGIS.
  • (P03) Devuelve información de OSM en formato JSON desde API REST de Nominatim a partir de unas coordenadas.

Este flujo podría corresponderse con un proyecto real que realiza un análisis de una capa del proyecto evaluando si hay cambios o discrepancias. Si existen, podríamos necesitar actualizar esas modificaciones en nuestra base de datos. Y para finalizar, queremos que todo el proceso de revisión quede almacenado en una aplicación corporativa a la accedemos mediante una API Rest previa autenticación.

Tras realizar estas comprobaciones, se presenta un mensaje indicado que las tres operaciones se han realizado correctamente.

Resultado final

Si alguno de los tres análisis no se realizar la ejecución finaliza. Esto se ha programado usando excepciones try/except. Pero el problema está que en el diseño original no se ofrece al usuario ningún mensaje sobre los motivos por lo que el complemento ha fallado (no encontramos la capa, la conexión a la base de datos no se ha realizado, no obtenemos los datos de la API…) por lo que la experiencia de usuario empieza a hacer aguas.

Información a nivel usuario

Nos basaremos en el diagrama de flujo que muestra los requisitos funcionales del desarrollo.

Flow chart

Vamos a usar los dos primeros procesos (P01 y P02) para ver las opciones que tenemos para comunicarnos con el usuario dentro de QGIS.

Función print()

Si no encontramos la capa points en nuestro proyecto vamos generar un aviso usando la función print() de Python.

El mensaje de salida usará literales de cadenas formateadas (f-string) para añadir el nombre de la capa. Esto nos permitiría pasar a una función el código y reaprovecharlo.

Completamos los mensajes devolviendo también el error de la excepción.

Todas las excepciones contarán con una instrucción return que forzará final de la función paralizando por tanto las demás revisiones.


def check():
    """Función que realiza una serie de comprobaciones y muestra el resultado en un mensaje
    """

    check_message = 'RESULTADO:\n\n'

    # P01 Busca una capa y devuelve su nombre
    LAYER = 'points'

    try:
        print('Check 01 Start')

        points_layer = QgsProject.instance().mapLayersByName(LAYER)[0]
        check_message += f'01 - La capa \'{points_layer.name()}\' se encuentra cargada.\n'

        print('Check 01 - OK')

    except Exception as error:
        error_message = f'Check 01 - FAIL - La capa {LAYER} no se encuentra en el proyecto'

        # 1 - Usamos print
        print(error_message)
        print(f'Error: {error}')
        return

Para realizar un test de nuestro código, vamos a cambiar el nombre de la capa a “points_2”, abriremos la Consola de Python de QGIS y ejecutamos nuestro complemento.

IMG

QgsMessageLog

Con la clase QgsMessaLog de PyQGIS seremos capaces de mostrar información en el Panel de Mensajes de Registro de QGIS.

...
    except Exception as error:
        error_message = f'Check 01 - FAIL - La capa {LAYER} no se encuentra en el proyecto'

        # 1 - Usamos print
        print(error_message)
        print(f'Error: {error}')

        # 2 - QgsMessageLog
        QgsMessageLog.logMessage(error_message, level=Qgis.Critical)
        QgsMessageLog.logMessage(
            f'Error: {error}', level=Qgis.Critical)

        return
...

IMG

QgsMessageBar

Las opciones anteriores requieren que el usuario tenga abierta bien la consola de Python o el Panel de Mensajes de Registro.

A no ser que se tenga cierto nivel de conocimiento de QGIS, estas dos funcionalidades nos van a pasar desapercibidas. Pueden ser de gran ayuda si estamos desarrollando nosotros mismos, pero si es un proyecto para un cliente no es la mejor opción.

La clase QgsMessageBar nos ayudará en este sentido.

La vamos a implementar en el segundo proceso (P02) donde realizamos una conexión a una base de datos usando la librería Python psycopg2

Forzamos el error introduciendo mal el nombre de la base de datos y vemos el resultado.

    ...
    # P02 Realiza una conexión a una DB
    conn = None

    try:
        conn = psycopg2.connect(host='localhost', port=5432, database='errordb', user='postgres', password='postgres')
        cur = conn.cursor()
        cur.close()

        check_message += f'02 - Conexión correcta a la db\n'

    except Exception as error:
        error_message = 'Check 02 - FAIL No se ha podido establecer la conexión a la DB'

        # 3 - messageBar()
        iface.messageBar().pushMessage("ERROR", error_message, level=Qgis.Critical)

        return

    finally:
        if conn is not None:
            conn.close()
    ...

IMG

Logging a nivel desarrollo

Todo lo mostrado hasta ahora fortalece la comunicación con el usuario de SIG. Pero como he comentado anteriormente, el registro de procesos y errores no es accesible para el desarrollador ya que es únicamente accesible en una sesión de trabajo. Cuando se cierre QGIS, todos esos datos se perderán.

Para poder generar un archivo con esta información, o lo que comúnmente se denomina un log de registro, usaremos otra librería estándar de Python denominada logging.

Con logging podremos programar el lanzamiento de mensajes de aviso con distintos niveles de severidad. Estos mensajes pueden ofrecerse por consola, almacenados en uno fichero o varios ficheros que puede ser guardados en local o remoto, formatear el texto de registro o integrarlo en varios módulos de nuestra librería. La documentación de la librería contiene cantidad de ejemplos.

En nuestro plugin configuramos el código para que cree un fichero de tipo .log en una carpeta concreta dentro del complemento de QGIS. En este fichero se irá registrando lo que ocurre en nuestra aplicación según un formato definido.

Nuestra tercera comprobación va a corresponder con un acceso y petición HTTP a la API abierta de geocodificación Nominatim basada en OpenStreetMap.

La petición GET será realizada con la librería urllib. Si pasamos por ejemplo una url inválida, no pasamos los parámetros correctos o no es posible realizar la conexión la excepción devolverá el mensaje de error y saldrá del flujo.

La librería urllib cuenta con su propia clase urllib.error para la gestión de errores que usaremos.

    # P03 Consulta una API
    URL_NOMINATIM = 'https://nominatim.openstreetmap.org/reverse?'
    COORDINATES = [37.87893, -4.772846]

    try:
        url = f'{URL_NOMINATIM}format=jsonv2&lat={COORDINATES[0]}&lon={COORDINATES[1]}'
        url_2 = f'{URL_NOMINATIM}format=jsonv2&lat={COORDINATES[0]}&lon='

        with urllib.request.urlopen(url) as response:
            data = response.read().decode('utf-8')
            print(data)

        msg += f'03 - Consulta a la API relizada\n'

    except urllib.error.URLError as e:
        return

Creamos una nueva función que hemos llamado create_loging_file() destinada a configurar y crear el log.

La función tendrá la responsabilidad de indicar el nombre y la ubicación del archivo, así como de su formato. Estos parámetros serán usados para crear una configuración básica del logger.

Una vez lanzada la función y ya dentro de la función principal check(), se ha definido la creación de fichero para que se vayan añadiendo las líneas de registro.


def create_loging_file():
    """Crear y configura un fichero logger de registro
    """

    log_path_save = Path(__file__).parent.absolute()
    log_filename = str(log_path_save)+"/log/logfile.log"
    log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'

    logging.basicConfig(
        level=logging.DEBUG,   # Nivel de los eventos que se registran en el logger
        filename=log_filename,  # Fichero de  logs
        format=log_format      # Formato de registro
    )

    if os.path.isfile(log_filename):
        with open(log_filename, 'a') as file:
            pass

    logging.info("############### START LOGGING ###############")


def check():
    """Función que realiza una serie de comprobaciones y muestra el resultado en un mensaje
    """

    create_loging_file()
    ...

Nos queda ya solo ir añadiendo los logs según los niveles que definamos. Sin duda dentro de las excepciones será de tipo ERROR.

...
    except urllib.error.URLError as error:
        error_message = 'Check 03 - FAIL Consulta a la API no relizada'

        # 4 - logging
        logging.error('Check 03 - FAIL')
        logging.error(error.reason)

        return
...

Si abrimos nuestro archivo de registro veremos que se han ido almacenando los mensajes configurados y que se han realizado dos llamadas en las que la URL de la API no estaba correcta y otra en la que la petición no estaba bien definida por obviar uno los parámetros.

API error

Versión final y conclusiones

El complemento terminaría definiendo los mensajes de registro de nivel INFO que también quisiéramos añadir en el archivo log. La versión final se encuentra en este repositorio de GitHub

Podríamos eliminar todas las salidas de la función print() ya que el usuario no las va a consultar.

Las opciones de logging son muy amplias y podemos llegar a tener varios archivos log, definir un módulo concreto que pueda ser usado para todo el código del complemento o añadir un archivo de configuración tipo .ini o en formato yaml… pero todo ya queda fuera del objetivo de esta entrada.

Como conclusión, solo quiero recalcar que contamos con las herramientas suficientes para ofrecer información a los usuarios de nuestros desarrollos esto nos va a reducir los quebraderos de cabeza.

Es también necesario tener en cuenta los datos que podamos ofrecer a nivel de diseño de la aplicación tanto si somos los desarrolladores del código como si debemos marcar los requisitos por ser el responsable del proyecto.

Recursos

Comentar