Monitorizar web con Python y Telegram 2

En el artículo anterior de como Monitorizar una web con Python y que nos avise por Telegram (Monitorizar web con Python y Telegram 1), hemos hablado de como crear un bot de Telegram que nos avisará si la web cae, y nos permite a su vez ejecutar un comando que remedie el problema.

Script en Python

Crear el script en Python

En estos punto vamos a proceder a crear el script, es importante ya que la intención no es llegar a desplegar una aplicación, sino tener un ejemplo de como con un script se pueden realizar tareas sin necesidad de desplegar aplicaciones que nos pueden llevar incluso más tiempo el asegurar su comportamiento.

Dependencias

Lo primero son las dependencias, este script tiene varios componentes que se reflejan en las librerías que va a usar: urllib3 para comprobar el estado web, psutil para comprobar si el script está en ejecución, daemon para que funcione como demonio, yaml para guardar la configuración de forma cómoda, pip para poder instalar la librería telegram-bot que nos permitirá usar Telegram.

Una instalación de dependencias en Debian/Ubuntu sería:

sudo apt-get update && sudo apt-get install -y curl python3 python3-urllib3 python3-psutil python3-daemon python3-yaml python3-setuptools python3-pip && sudo pip3 install python-telegram-bot

Más documentación sobre la librerías en:

Argumentos

La aplicación se ha configurado para que tenga el formato:

command CONFIGFILE.yml [start|stop|status|reload]

Este formato nos permitirá integrarlo con systemd para poder, por ejemplo, ejecutarlo al iniciar el equipo.

Configuración

CONFIGFILE.yml es la ruta completa del archivo de configuración que tendrá formato siguiente:

LOCK_WAIT_TIMEOUT: 5
CHAT_ID: TELEGRAM_PRIVATE_CHANNEL_ID
TOKEN: TELEGRAM_BOT_TOKEN
PIDFILE: '/var/run/disasterproject_bot.pid'
MONITOR_NAME: "Disasterproject"
MONITOR_URL: "https://www.disasterproject.com"
CMD: "sudo /usr/local/bin/executable"

Podemos ver los siguientes valores de configuración:

  • LOCK_WAIT_TIMEOUT: Tiempo de espera para comprobar que la aplicación ya está funcionando.
  • CHAT_ID: El identificador del canal privado que hemos obtenido en el artículo anterior
  • TOKEN: El identificador del Bot de Terraform
  • PIDFILE: Fichero que guarda el identificador del proceso y que se bloqueará mientras la aplicación esté en uso
  • MONITOR_NAME: Nombre identificador que aparecerá en el bot
  • MONITOR_URL: URL que queremos monitorizar
  • CMD: Comando que se ejecutará a través del bot

La potencia de disponer de una configuración en yaml es que el proceso de carga es realmente simple, siendo el principal componente un simple “yaml.load(file)”, en el siguiente trozo de código vemos una función con parámetro el fichero yaml y que nos devuelve un hash con los valores.

def readConf(path="/etc/check/config.yml"):
    try:
        with open(path, 'r') as ymlfile:
            return yaml.load(ymlfile)
    except IOError:
        sys.stderr.write(
            "Error: Configuration file %s does not appear to exist."
            % path)
        sys.exit(1)
    except ImportError:
        sys.stderr.write(
            "Error: Configuration file %s is not valid." % path)
        sys.exit(1)

Registrar eventos

Un punto importante en cualquier script es que vuelque la suficiente información como para identificar que se está haciendo.

La siguiente función devuelve un manejador de eventos, que llamaremos con logger = getLogger() y que usaremos con logger.info(cfg['MONITOR_NAME'] + " mensaje")

def getLogger():
    logging.basicConfig(
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        level=logging.INFO)
    logger = logging.getLogger(cfg['MONITOR_NAME'])
    handler = logging.handlers.SysLogHandler(address='/dev/log')
    logger.addHandler(handler)
    return logger

Funcionando como demonio

Muchas veces es interesante que el script funcione de manera autónoma sin tener que ejecutarlo de manera periódica, para ello lo ejecutaremos en modo demonio, comprobando de manera periódica tanto la monitorización como la gestión del bot de Telegram.

Hemos elegido la librería python-daemon por ser muy sencilla de utilizar y permitir un control sencillo sobre las señales (que usaremos para gestionar el ciclo de vida del script) y el contexto (donde realmente se ejecutará el script una vez cargados los argumentos y la configuración). La estructura de un script para demonio sería como la siguiente:

context = daemon.DaemonContext(
    # stdout=sys.stdout,
    stderr=sys.stderr,
    chroot_directory=None,
    working_directory='/tmp',
    umask=0o002,
    pidfile=pid,
    )

context.signal_map = {
    signal.SIGTERM: program_cleanup,
    signal.SIGHUP: end_program,
    signal.SIGUSR1: reload_program_config,
    }

with context:
    do_main_program()

Lo primero que hacemos en este trozo de código es definir un contexto para ejecutar el bloque principal del programa (función do_main_program), y sobre dicho contexto vamos a definir tres funciones que se van a activar únicamente cuando el script recibe una señal. Las señales servirán en otra parte del script para controlar el ciclo de vida del proceso, especialmente para parar la aplicación.

Hilos de proceso

Una vez dentro del contexto del demonio, nos encontramos con la necesidad de comprobar de manera continua y periódica dos eventos: los de monitorización y los correspondientes al bot de Telegram.

Por simplificar hemos dejar una ejecución principal para el bot de Telegram y hemos llevado la monitorización a una función que se ejecuta de manera separada e independiente a través del uso de hilos.

    check_thread = threading.Thread(
        target=monitorProc, args=(updater, logger, ))
    check_thread.start()

Con el código anterior se define un nuevo hilo donde se ejecutará la función monitorProc y como argumentos updater (el conector con el bot de Telegram) y un apuntado al gestor de eventos.

def monitorProc(updater, logger):
    t = threading.currentThread()
    t.noStop = True
    while getattr(t, "noStop", True):
        sleep(10)
        if not monitor():
            updater.bot.send_message(
                chat_id=cfg['CHAT_ID'],
                text=cfg['MONITOR_NAME'] + " Status: ERROR")
            sleep(540)
    logger.info(cfg['MONITOR_NAME'] + " - Monitor Stopped")

La función llamada por el nuevo hilo tiene una serie de puntos importantes:

  • Llama a la función monitor y envía un mensaje al bot si el valor es incorrecto
  • Comprueba si en este hilo, el parámetro noStop es cierto o verdadero (funciona como semáforo), está comprobación nos servirá para poder parar el hilo sin problemas desde el proceso principal
  • Mientras noStop sea cierto se procesa en bucle, si encuentra un error espera un tiempo prudencial hasta la próxima comprobación.

Siguiente artículo

Hasta aquí hemos definido el diseño del bot y la estructura básica del script, en el próximo artículo definiremos el bot, como monitorizar un sitio web y el ciclo de vida del script.

Volver arriba