En este artículo, exploraremos cómo usar Django Channels y Celery para crear una funcionalidad de seguimiento de archivos de registro (similar a tail -f) en una aplicación web. Permitirá a los usuarios autenticados monitorear registros en tiempo real a través de WebSocket, sin interferir entre sesiones.
Configuración tecnológica
El código se basa en las siguientes versiones de software:
- Python 3.6.3
- Django 2.2
- Channesl 2.1.7
- Celery 4.3.0
Nota: Celery 4 no tiene soporte completo en Windows, por lo que se recomineda ejecutar pruebas en Linux.
Definición de datos de registro
En lugar de usar una base de datos, almacenaremos las rutas de los archivos de registro en un diccionario dentro de settings.py. Esto simplifica la gestión para un conjunto fijo de archivos.
# En settings.py
ARCHIVOS_REGISTRO = {
'archivo_1': '/var/log/mi_app/errores.log',
'archivo_2': '/var/log/mi_app/acceso.log',
}
Construcción de la página web base
Asumimos que ya existe una aplicación Django llamada monitor_registros integrada en INSTALLED_APPS. La estructura del proyecto incluye vistas, plantillas y rutas estándar.
Configuración de URLs
from django.urls import path
from django.contrib.auth.views import LoginView, LogoutView
from monitor_registros.views import vista_monitor
urlpatterns = [
path('monitor/', vista_monitor, name='monitor-url'),
path('login/', LoginView.as_view(template_name='login.html'), name='login-url'),
path('logout/', LogoutView.as_view(template_name='login.html'), name='logout-url'),
]
Utilizamos las vistas de autenticación integradas de Django para manejar el inicio y cierre de sesión.
Vistas
from django.conf import settings
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
@login_required(login_url='/login')
def vista_monitor(request):
registros = settings.ARCHIVOS_REGISTRO
return render(request, 'monitor/index.html', {'registros': registros})
El decorador login_required asegura que solo los usuarios autenticados accedan a la vista. El diccionario de archivos se pasa a la plantilla.
Plantilla HTML
{% extends "base.html" %}
{% block content %}
<div class="selector-archivo">
<select id="seleccion-archivo">
<option value="">Seleccione un archivo de registro</option>
{% for clave, ruta in registros.items %}
<option value="{{ clave }}">{{ ruta }}</option>
{% endfor %}
</select>
</div>
<div class="controles">
<button id="btn-iniciar" onclick="iniciarMonitoreo()">Iniciar Monitoreo</button>
<button id="btn-detener" onclick="detenerMonitoreo()">Detener Monitoreo</button>
</div>
<div class="area-registros">
<textarea id="salida-registros" readonly="readonly" rows="20"></textarea>
</div>
{% endblock %}
La plantilla muestra un menú desplegable con los archivos disponibles y botones para controlar el monitoreo.
Integración de Channels para WebSocket
El diseño implica establecer una conexión WebSocket entre la página y el servidor. Celery ejecuta de forma asíncrona un bucle que lee el archivo de registro y envía actualizaciones al canal de WebSocket.
Enrutamiento de Channels
Modificamos el archivo de enrutamiento para incluir la ruta WebSocket específica.
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import re_path
from monitor_registros.consumers import ConsumidorRegistro
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter([
re_path(r'^ws/monitor/(?P<id_archivo>\w+)/$', ConsumidorRegistro.as_asgi()),
])
)
})</id_archivo>
La ruta utiliza una expresión regular para capturar el identificador del archivo de registro.
Consumer de Channels
import json
from channels.generic.websocket import WebsocketConsumer
from monitor_registros.tareas import tarea_seguir_registro
class ConsumidorRegistro(WebsocketConsumer):
def connect(self):
self.id_archivo = self.scope['url_route']['kwargs']['id_archivo']
self.tarea_celery = tarea_seguir_registro.delay(self.id_archivo, self.channel_name)
self.accept()
def disconnect(self, close_code):
if hasattr(self, 'tarea_celery'):
self.tarea_celery.revoke(terminate=True)
def enviar_mensaje(self, event):
self.send(text_data=json.dumps({
'contenido': event['mensaje']
}))
El consumer inicia una tarea de Celery al conectarse y la revoca al desconectarse. El método enviar_mensaje maneja el envío de datos al cliente.
Tarea de Celery para leer registros
Implementamos la tarea de Celery que monitorea el archivo y envía actualizaciones a través de Channels.
from celery import shared_task
import time
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from django.conf import settings
@shared_task
def tarea_seguir_registro(id_archivo, nombre_canal):
capa_canal = get_channel_layer()
ruta_archivo = settings.ARCHIVOS_REGISTRO.get(id_archivo)
if not ruta_archivo:
return
try:
with open(ruta_archivo, 'r') as archivo:
archivo.seek(0, 2) # Mover al final del archivo
while True:
linea = archivo.readline()
if linea:
async_to_sync(capa_canal.send)(
nombre_canal,
{
'type': 'enviar.mensaje',
'mensaje': linea.strip()
}
)
else:
time.sleep(0.5)
except FileNotFoundError:
pass
La tarea lee el archivo continuamente desde el final y envía nuevas líneas al canal WebSocket. Se usa async_to_sync para integrar con la capa de canales asíncrona.
Soporte WebSocket en el frontend
Agregamos JavaScript para manejar la conexión WebSocket y actualizar la interfaz de usuario.
function iniciarMonitoreo() {
const idArchivo = document.getElementById('seleccion-archivo').value;
if (!idArchivo) {
alert('Seleccione un archivo de registro');
return;
}
window.socketMonitoreo = new WebSocket(
`ws://${window.location.host}/ws/monitor/${idArchivo}/`
);
window.socketMonitoreo.onmessage = function(event) {
const datos = JSON.parse(event.data);
const areaRegistros = document.getElementById('salida-registros');
areaRegistros.value += datos.contenido + '\n';
areaRegistros.scrollTop = areaRegistros.scrollHeight;
};
window.socketMonitoreo.onerror = function() {
console.error('Error en la conexión WebSocket');
};
}
function detenerMonitoreo() {
if (window.socketMonitoreo) {
window.socketMonitoreo.close();
window.socketMonitoreo = null;
}
}
La función iniciarMonitoreo establece la conexión y detenerMonitoreo la cierra, lo que desencadena la revocación de la tarea de Celery en el backend.
Desconexión activa desde la web
Al hacer clic en "Detener Monitoreo", se llama a detenerMonitoreo, que cierra el WebSocket. Esto activa el método disconnect en el consumer, revocando la tarea de Celery y liberando recursos.
Esta implementación proporciona una solución completa para el monitoreo de registros en tiempo real, permitiendo a múltiples usuarios trabajar simultáneamente sin interferencias.