Initial commit

This commit is contained in:
2026-01-12 08:17:17 -07:00
commit de5b6feef4
104 changed files with 12925 additions and 0 deletions

279
workers/README.md Normal file
View File

@@ -0,0 +1,279 @@
# ServiceManagerWeb Workers
Workers asíncronos con Celery para el sistema de Mesa de Ayuda B2B.
## Estructura
```
workers/
├── app/
│ ├── celery.py # Configuración principal de Celery
│ ├── core/ # Configuración compartida
│ │ ├── config.py # Settings para workers
│ │ └── logging.py # Logging estructurado
│ └── tasks/ # Tareas por dominio
│ ├── email_tasks.py # Envío de emails
│ ├── sla_tasks.py # Monitoreo de SLAs
│ ├── maintenance_tasks.py # Mantenimiento del sistema
│ └── notification_tasks.py # Notificaciones y digests
├── requirements.txt # Dependencias Python
└── README.md # Esta documentación
```
## Tareas Implementadas
### Email Tasks (`email_tasks.py`)
- [x] `send_email_task`: Envío básico de emails SMTP
- [x] `send_templated_email_task`: Emails con plantillas Jinja2
- [x] `send_bulk_email_task`: Envío masivo con progreso
### SLA Tasks (`sla_tasks.py`)
- [x] `check_sla_violations`: Monitoreo de violaciones SLA
- [x] `calculate_sla_metrics`: Cálculo de métricas SLA
- [x] `send_sla_warnings`: Alertas de SLAs próximos a vencer
### Maintenance Tasks (`maintenance_tasks.py`)
- [x] `health_check`: Health check de workers
- [x] `cleanup_old_logs`: Limpieza de logs antiguos
- [x] `generate_weekly_reports`: Reportes semanales
- [x] `cleanup_temp_files`: Limpieza de archivos temporales
- [x] `database_maintenance`: Mantenimiento de BD
### Notification Tasks (`notification_tasks.py`)
- [x] `send_daily_digest`: Digest diario para agentes
- [x] `send_ticket_notifications`: Notificaciones de tickets
- [x] `send_system_alert`: Alertas del sistema
## Programación Automática (Celery Beat)
### Tareas Periódicas Configuradas
```python
# Cada 5 minutos
"check-sla-violations": check_sla_violations
# Diario a las 8:00 AM
"send-daily-digest": send_daily_digest
# Semanal los domingos a las 2:00 AM
"cleanup-old-logs": cleanup_old_logs
# Semanal los lunes a las 9:00 AM
"generate-weekly-reports": generate_weekly_reports
# Cada minuto (health check)
"worker-health-check": health_check
```
## Quick Start
### Desarrollo Local
```bash
# Instalar dependencias
pip install -r requirements.txt
# Variables de entorno (usar las del proyecto principal)
cp ../.env.example .env
# Ejecutar worker
celery -A app.celery worker --loglevel=info
# Ejecutar beat scheduler (en otra terminal)
celery -A app.celery beat --loglevel=info
# Monitoreo con Flower (opcional)
celery -A app.celery flower
```
### Con Docker
```bash
# Worker y Beat se ejecutan automáticamente con docker-compose
docker-compose up worker beat
# Ver logs
docker-compose logs -f worker
docker-compose logs -f beat
```
## Configuración
### Variables de Entorno Importantes
```bash
# Celery
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
# Email
SMTP_HOST=mailhog
SMTP_PORT=1025
DEFAULT_FROM_EMAIL=noreply@servicemanager.local
# SLA
SLA_CHECK_ENABLED=true
SLA_WARNING_THRESHOLD=0.8
# Mantenimiento
LOG_RETENTION_DAYS=30
DIGEST_ENABLED=true
```
### Colas de Trabajo
- **default**: Tareas generales
- **email**: Envío de emails
- **sla**: Monitoreo SLA
- **maintenance**: Mantenimiento
- **notifications**: Notificaciones
## Monitoreo
### Logs Estructurados
Todos los workers utilizan structured logging con:
- Task ID único
- Correlation ID para tracking
- Contexto de tenant
- Métricas de performance
### Health Checks
```bash
# Health check manual
celery -A app.celery inspect ping
# Estadísticas de workers
celery -A app.celery inspect stats
# Tareas activas
celery -A app.celery inspect active
```
### Métricas
- Task execution times
- Success/failure rates
- Queue lengths
- Worker load
## Desarrollo
### Agregar Nueva Tarea
1. Crear función en módulo apropiado:
```python
@celery_app.task(bind=True, time_limit=300)
def new_task(self, param1: str, param2: int):
logger = get_logger(__name__)
# Implementation
return result
```
2. Registrar en `celery.py` si es periódica:
```python
beat_schedule = {
"new-periodic-task": {
"task": "app.tasks.module.new_task",
"schedule": crontab(minute=0, hour=9),
}
}
```
3. Agregar tests en `tests/`
### Retry y Error Handling
```python
@celery_app.task(
bind=True,
autoretry_for=(ConnectionError, TimeoutError),
retry_kwargs={'max_retries': 3, 'countdown': 60}
)
def reliable_task(self):
# Task que se reintenta automáticamente
pass
```
### Templates de Email
Los templates están definidos en código por ahora. En el futuro se moverán a base de datos para ser editables por tenants.
Templates disponibles:
- `ticket_created`
- `ticket_assigned`
- `ticket_resolved`
- `sla_warning`
- `sla_violation`
- `daily_digest`
- `system_alert`
## Testing
```bash
# Ejecutar tests
pytest
# Tests específicos de workers
pytest tests/test_tasks/
# Ejecutar tarea manualmente para testing
celery -A app.celery call app.tasks.email_tasks.send_email_task --args='["test@example.com", "Test Subject", "<h1>Test</h1>"]'
```
## Producción
### Configuración Recomendada
```bash
# Múltiples workers por queue
celery -A app.celery worker --loglevel=info --concurrency=4 --queues=email
celery -A app.celery worker --loglevel=info --concurrency=2 --queues=sla,maintenance
celery -A app.celery worker --loglevel=info --concurrency=1 --queues=default
# Beat scheduler (solo una instancia)
celery -A app.celery beat --loglevel=info
# Con systemd o supervisor para auto-restart
```
### Optimizaciones
- Pool de conexiones Redis
- Compresión de mensajes grandes
- Rate limiting por tarea
- Monitoring con Prometheus/Grafana
## Troubleshooting
### Problemas Comunes
1. **Tasks stuck in queue**:
- Verificar workers activos
- Revisar configuración de routing
2. **Memory leaks**:
- Configurar `worker_max_tasks_per_child`
- Monitorear uso de memoria
3. **Email delivery failures**:
- Verificar configuración SMTP
- Revisar logs de tareas email
4. **SLA false positives**:
- Verificar timezones
- Validar lógica de cálculo
### Debug
```bash
# Ejecutar worker en modo debug
celery -A app.celery worker --loglevel=debug
# Inspeccionar tareas fallidas
celery -A app.celery inspect failed
# Purgar queue
celery -A app.celery purge -Q queue_name
```

129
workers/app/celery.py Normal file
View File

@@ -0,0 +1,129 @@
"""
Celery Application - ServiceManagerWeb Workers
Configuración principal de Celery para tareas asíncronas
"""
from celery import Celery
from celery.schedules import crontab
import os
from app.core.config import get_settings
from app.core.logging import setup_logging
# Setup logging
setup_logging()
# Get settings
settings = get_settings()
# Create Celery application
celery_app = Celery(
"servicemanager-workers",
broker=settings.CELERY_BROKER_URL,
backend=settings.CELERY_RESULT_BACKEND,
include=[
"app.tasks.email_tasks",
"app.tasks.sla_tasks",
"app.tasks.maintenance_tasks",
"app.tasks.notification_tasks"
]
)
# Configure Celery
celery_app.conf.update(
# Task settings
task_serializer="json",
accept_content=["json"],
result_serializer="json",
timezone="UTC",
enable_utc=True,
# Result backend settings
result_expires=3600, # 1 hour
result_persistent=True,
# Worker settings
worker_prefetch_multiplier=1,
worker_max_tasks_per_child=1000,
worker_disable_rate_limits=False,
# Task routing
task_routes={
"app.tasks.email_tasks.*": {"queue": "email"},
"app.tasks.sla_tasks.*": {"queue": "sla"},
"app.tasks.maintenance_tasks.*": {"queue": "maintenance"},
"app.tasks.notification_tasks.*": {"queue": "notifications"},
},
# Queue configuration
task_default_queue="default",
task_default_exchange="default",
task_default_routing_key="default",
# Beat schedule for periodic tasks
beat_schedule={
# Check SLA violations every 5 minutes
"check-sla-violations": {
"task": "app.tasks.sla_tasks.check_sla_violations",
"schedule": crontab(minute="*/5"),
},
# Send daily digest at 8:00 AM
"send-daily-digest": {
"task": "app.tasks.notification_tasks.send_daily_digest",
"schedule": crontab(hour=8, minute=0),
},
# Clean old logs weekly on Sunday at 2:00 AM
"cleanup-old-logs": {
"task": "app.tasks.maintenance_tasks.cleanup_old_logs",
"schedule": crontab(hour=2, minute=0, day_of_week=0),
},
# Generate weekly reports on Monday at 9:00 AM
"generate-weekly-reports": {
"task": "app.tasks.maintenance_tasks.generate_weekly_reports",
"schedule": crontab(hour=9, minute=0, day_of_week=1),
},
# Health check every minute
"worker-health-check": {
"task": "app.tasks.maintenance_tasks.health_check",
"schedule": crontab(minute="*/1"),
},
},
# Error handling
task_reject_on_worker_lost=True,
task_acks_late=True,
# Monitoring
worker_send_task_events=True,
task_send_sent_event=True,
# Security
worker_hijack_root_logger=False,
worker_log_format="[%(asctime)s: %(levelname)s/%(processName)s] %(message)s",
worker_task_log_format="[%(asctime)s: %(levelname)s/%(processName)s][%(task_name)s(%(task_id)s)] %(message)s",
)
# Optional: Configure SSL if needed
if settings.ENVIRONMENT == "production":
# Enable SSL for production
celery_app.conf.update(
broker_use_ssl=True,
redis_backend_use_ssl=True,
)
# Import all tasks to register them
from app.tasks import (
email_tasks,
sla_tasks,
maintenance_tasks,
notification_tasks
)
if __name__ == "__main__":
celery_app.start()

117
workers/app/core/config.py Normal file
View File

@@ -0,0 +1,117 @@
"""
Core Configuration for Workers - ServiceManagerWeb
Configuración compartida entre workers usando Pydantic Settings
"""
from functools import lru_cache
from typing import List, Optional
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
import os
class WorkerSettings(BaseSettings):
"""Configuración para workers Celery."""
# ===================================
# GENERAL
# ===================================
ENVIRONMENT: str = Field(default="development", env="ENVIRONMENT")
DEBUG: bool = Field(default=False, env="DEBUG")
# ===================================
# DATABASE
# ===================================
DATABASE_URL: str = Field(..., env="DATABASE_URL")
# ===================================
# CELERY & REDIS
# ===================================
CELERY_BROKER_URL: str = Field(..., env="CELERY_BROKER_URL")
CELERY_RESULT_BACKEND: str = Field(..., env="CELERY_RESULT_BACKEND")
REDIS_URL: str = Field(..., env="REDIS_URL")
# ===================================
# EMAIL SETTINGS
# ===================================
SMTP_HOST: str = Field(default="localhost", env="SMTP_HOST")
SMTP_PORT: int = Field(default=587, env="SMTP_PORT")
SMTP_USER: Optional[str] = Field(default=None, env="SMTP_USER")
SMTP_PASSWORD: Optional[str] = Field(default=None, env="SMTP_PASSWORD")
SMTP_USE_TLS: bool = Field(default=True, env="SMTP_USE_TLS")
SMTP_USE_SSL: bool = Field(default=False, env="SMTP_USE_SSL")
DEFAULT_FROM_EMAIL: str = Field(default="noreply@servicemanager.local", env="DEFAULT_FROM_EMAIL")
DEFAULT_FROM_NAME: str = Field(default="ServiceManager", env="DEFAULT_FROM_NAME")
# Email retry settings
EMAIL_MAX_RETRIES: int = Field(default=3, env="EMAIL_MAX_RETRIES")
EMAIL_RETRY_DELAY: int = Field(default=60, env="EMAIL_RETRY_DELAY") # seconds
# ===================================
# FILE PROCESSING
# ===================================
UPLOAD_PATH: str = Field(default="/app/uploads", env="UPLOAD_PATH")
MAX_UPLOAD_SIZE_MB: int = Field(default=10, env="MAX_UPLOAD_SIZE_MB")
# ===================================
# SLA SETTINGS
# ===================================
SLA_CHECK_ENABLED: bool = Field(default=True, env="SLA_CHECK_ENABLED")
SLA_WARNING_THRESHOLD: float = Field(default=0.8, env="SLA_WARNING_THRESHOLD") # 80% of SLA time
# ===================================
# LOGGING
# ===================================
LOG_LEVEL: str = Field(default="INFO", env="LOG_LEVEL")
LOG_FORMAT: str = Field(default="json", env="LOG_FORMAT")
LOG_FILE: Optional[str] = Field(default=None, env="LOG_FILE")
# ===================================
# MONITORING
# ===================================
SENTRY_DSN: Optional[str] = Field(default=None, env="SENTRY_DSN")
PROMETHEUS_PORT: int = Field(default=8888, env="PROMETHEUS_PORT")
# ===================================
# TASK SETTINGS
# ===================================
TASK_TIME_LIMIT: int = Field(default=300, env="TASK_TIME_LIMIT") # 5 minutes
TASK_SOFT_TIME_LIMIT: int = Field(default=240, env="TASK_SOFT_TIME_LIMIT") # 4 minutes
# ===================================
# MAINTENANCE SETTINGS
# ===================================
LOG_RETENTION_DAYS: int = Field(default=30, env="LOG_RETENTION_DAYS")
ATTACHMENT_RETENTION_DAYS: int = Field(default=90, env="ATTACHMENT_RETENTION_DAYS")
# ===================================
# NOTIFICATION SETTINGS
# ===================================
NOTIFICATION_BATCH_SIZE: int = Field(default=100, env="NOTIFICATION_BATCH_SIZE")
DIGEST_ENABLED: bool = Field(default=True, env="DIGEST_ENABLED")
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
"case_sensitive": True
}
def is_production(self) -> bool:
"""Check if environment is production."""
return self.ENVIRONMENT.lower() == "production"
def is_development(self) -> bool:
"""Check if environment is development."""
return self.ENVIRONMENT.lower() == "development"
@lru_cache()
def get_settings() -> WorkerSettings:
"""
Get cached settings instance.
Using lru_cache to create a singleton pattern for settings.
"""
return WorkerSettings()

122
workers/app/core/logging.py Normal file
View File

@@ -0,0 +1,122 @@
"""
Logging Configuration for Workers - ServiceManagerWeb
Configuración de logging estructurado para workers Celery
"""
import logging
import logging.config
import sys
from typing import Any, Dict
import structlog
from app.core.config import get_settings
settings = get_settings()
def setup_logging():
"""Configure structured logging for workers."""
processors = [
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
# Add worker-specific context
structlog.processors.CallsiteParameterAdder(
parameters=[
structlog.processors.CallsiteParameter.FUNC_NAME,
structlog.processors.CallsiteParameter.PATHNAME,
structlog.processors.CallsiteParameter.LINENO,
]
),
]
if settings.LOG_FORMAT == "json":
processors.append(structlog.processors.JSONRenderer())
else:
processors.append(structlog.dev.ConsoleRenderer(colors=True))
structlog.configure(
processors=processors,
wrapper_class=structlog.stdlib.BoundLogger,
logger_factory=structlog.stdlib.LoggerFactory(),
context_class=dict,
cache_logger_on_first_use=True,
)
# Configure standard library logging for Celery
logging_config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.JSONRenderer(),
},
"console": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.dev.ConsoleRenderer(colors=True),
},
},
"handlers": {
"console": {
"level": settings.LOG_LEVEL,
"class": "logging.StreamHandler",
"stream": sys.stdout,
"formatter": "json" if settings.LOG_FORMAT == "json" else "console",
},
},
"loggers": {
"": { # root logger
"handlers": ["console"],
"level": settings.LOG_LEVEL,
"propagate": False,
},
"celery": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"celery.worker": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"celery.task": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
"app": {
"handlers": ["console"],
"level": settings.LOG_LEVEL,
"propagate": False,
},
},
}
# Add file handler if specified
if settings.LOG_FILE:
logging_config["handlers"]["file"] = {
"level": settings.LOG_LEVEL,
"class": "logging.handlers.RotatingFileHandler",
"filename": settings.LOG_FILE,
"maxBytes": 10 * 1024 * 1024, # 10MB
"backupCount": 5,
"formatter": "json",
}
for logger_config in logging_config["loggers"].values():
logger_config["handlers"].append("file")
logging.config.dictConfig(logging_config)
def get_logger(name: str = None) -> structlog.BoundLogger:
"""Get a configured structlog logger."""
return structlog.get_logger(name)

View File

@@ -0,0 +1,404 @@
"""
Email Tasks - ServiceManagerWeb Workers
Tareas asíncronas para envío de emails y notificaciones
"""
from celery import current_task
from celery.exceptions import Retry
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import smtplib
import ssl
from typing import Dict, List, Optional, Any
from jinja2 import Template, Environment, BaseLoader
import structlog
from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
settings = get_settings()
logger = get_logger(__name__)
class MemoryLoader(BaseLoader):
"""Jinja2 loader for templates from memory/database."""
def __init__(self, templates: Dict[str, str]):
self.templates = templates
def get_source(self, environment, template):
if template in self.templates:
source = self.templates[template]
return source, None, lambda: True
raise TemplateNotFoundError(template)
@celery_app.task(
bind=True,
autoretry_for=(Exception,),
retry_kwargs={'max_retries': 3, 'countdown': 60},
time_limit=120,
soft_time_limit=90
)
def send_email_task(
self,
to_email: str,
subject: str,
html_content: str,
text_content: Optional[str] = None,
from_email: Optional[str] = None,
from_name: Optional[str] = None,
attachments: Optional[List[Dict[str, Any]]] = None,
tenant_id: Optional[str] = None,
correlation_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send email using SMTP.
Args:
to_email: Recipient email address
subject: Email subject
html_content: HTML content
text_content: Plain text content (optional)
from_email: Sender email (optional, uses default)
from_name: Sender name (optional)
attachments: List of attachment dicts
tenant_id: Tenant ID for logging
correlation_id: Correlation ID for tracking
Returns:
Dict with send result
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id,
correlation_id=correlation_id
)
task_logger.info(
"Starting email send task",
to_email=to_email,
subject=subject
)
try:
# Prepare email
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = f"{from_name or settings.DEFAULT_FROM_NAME} <{from_email or settings.DEFAULT_FROM_EMAIL}>"
msg['To'] = to_email
# Add text content
if text_content:
text_part = MIMEText(text_content, 'plain', 'utf-8')
msg.attach(text_part)
# Add HTML content
html_part = MIMEText(html_content, 'html', 'utf-8')
msg.attach(html_part)
# Add attachments
if attachments:
for attachment in attachments:
part = MIMEBase('application', 'octet-stream')
part.set_payload(attachment['content'])
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
f'attachment; filename= {attachment["filename"]}'
)
msg.attach(part)
# Send email
context = ssl.create_default_context()
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server:
if settings.SMTP_USE_TLS:
server.starttls(context=context)
if settings.SMTP_USER and settings.SMTP_PASSWORD:
server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
server.send_message(msg)
task_logger.info(
"Email sent successfully",
to_email=to_email,
subject=subject
)
return {
"success": True,
"to_email": to_email,
"subject": subject,
"sent_at": current_task.request.eta or "now"
}
except Exception as exc:
task_logger.error(
"Failed to send email",
to_email=to_email,
subject=subject,
error=str(exc),
exc_info=True
)
# Check if we should retry
if self.request.retries < self.max_retries:
task_logger.info(
"Retrying email send",
retry_count=self.request.retries + 1,
max_retries=self.max_retries
)
raise self.retry(countdown=60 * (2 ** self.request.retries))
return {
"success": False,
"to_email": to_email,
"subject": subject,
"error": str(exc)
}
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def send_templated_email_task(
self,
to_email: str,
template_name: str,
context: Dict[str, Any],
tenant_id: Optional[str] = None,
correlation_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send email using a template.
Args:
to_email: Recipient email
template_name: Template identifier
context: Template context variables
tenant_id: Tenant ID
correlation_id: Correlation ID
Returns:
Dict with send result
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id,
correlation_id=correlation_id
)
task_logger.info(
"Starting templated email task",
to_email=to_email,
template_name=template_name
)
try:
# TODO: Fetch template from database
# For now, use mock templates
templates = {
"ticket_created": {
"subject": "Nuevo ticket #{{ ticket_number }}: {{ subject }}",
"html": """
<h2>Nuevo ticket creado</h2>
<p>Hola {{ user_name }},</p>
<p>Se ha creado un nuevo ticket:</p>
<ul>
<li><strong>Número:</strong> #{{ ticket_number }}</li>
<li><strong>Asunto:</strong> {{ subject }}</li>
<li><strong>Prioridad:</strong> {{ priority }}</li>
</ul>
<p><a href="{{ ticket_url }}">Ver ticket</a></p>
<p>Saludos,<br>Equipo de Soporte</p>
""",
"text": """
Nuevo ticket creado
Hola {{ user_name }},
Se ha creado un nuevo ticket:
Número: #{{ ticket_number }}
Asunto: {{ subject }}
Prioridad: {{ priority }}
Ver ticket: {{ ticket_url }}
Saludos,
Equipo de Soporte
"""
},
"ticket_assigned": {
"subject": "Ticket #{{ ticket_number }} asignado a ti",
"html": """
<h2>Ticket asignado</h2>
<p>Hola {{ agent_name }},</p>
<p>Se te ha asignado el ticket:</p>
<ul>
<li><strong>Número:</strong> #{{ ticket_number }}</li>
<li><strong>Asunto:</strong> {{ subject }}</li>
<li><strong>Cliente:</strong> {{ customer_name }}</li>
<li><strong>Prioridad:</strong> {{ priority }}</li>
</ul>
<p><a href="{{ ticket_url }}">Ver ticket</a></p>
""",
"text": """
Ticket asignado
Hola {{ agent_name }},
Se te ha asignado el ticket:
Número: #{{ ticket_number }}
Asunto: {{ subject }}
Cliente: {{ customer_name }}
Prioridad: {{ priority }}
Ver ticket: {{ ticket_url }}
"""
}
}
if template_name not in templates:
raise ValueError(f"Template '{template_name}' not found")
template_data = templates[template_name]
# Render templates
env = Environment(loader=MemoryLoader({
f"{template_name}_subject": template_data["subject"],
f"{template_name}_html": template_data["html"],
f"{template_name}_text": template_data["text"]
}))
subject_template = env.get_template(f"{template_name}_subject")
html_template = env.get_template(f"{template_name}_html")
text_template = env.get_template(f"{template_name}_text")
subject = subject_template.render(**context)
html_content = html_template.render(**context)
text_content = text_template.render(**context)
# Send email using the basic send task
return send_email_task.apply_async(
kwargs={
"to_email": to_email,
"subject": subject,
"html_content": html_content,
"text_content": text_content,
"tenant_id": tenant_id,
"correlation_id": correlation_id
}
).get()
except Exception as exc:
task_logger.error(
"Failed to send templated email",
to_email=to_email,
template_name=template_name,
error=str(exc),
exc_info=True
)
return {
"success": False,
"to_email": to_email,
"template_name": template_name,
"error": str(exc)
}
@celery_app.task(
bind=True,
time_limit=600,
soft_time_limit=540
)
def send_bulk_email_task(
self,
email_list: List[Dict[str, Any]],
tenant_id: Optional[str] = None,
correlation_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send bulk emails.
Args:
email_list: List of email dicts with to_email, subject, content
tenant_id: Tenant ID
correlation_id: Correlation ID
Returns:
Dict with bulk send results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id,
correlation_id=correlation_id
)
total_emails = len(email_list)
task_logger.info(f"Starting bulk email task", total_emails=total_emails)
results = []
for i, email_data in enumerate(email_list):
try:
result = send_email_task.apply_async(
kwargs={
**email_data,
"tenant_id": tenant_id,
"correlation_id": correlation_id
}
).get()
results.append(result)
# Update task progress
current_task.update_state(
state='PROGRESS',
meta={'current': i + 1, 'total': total_emails}
)
except Exception as exc:
task_logger.error(
"Failed to send bulk email item",
index=i,
email_data=email_data,
error=str(exc)
)
results.append({
"success": False,
"to_email": email_data.get("to_email"),
"error": str(exc)
})
# Calculate stats
successful = sum(1 for r in results if r.get("success"))
failed = total_emails - successful
task_logger.info(
"Bulk email task completed",
total=total_emails,
successful=successful,
failed=failed
)
return {
"total": total_emails,
"successful": successful,
"failed": failed,
"results": results
}

View File

@@ -0,0 +1,371 @@
"""
Maintenance Tasks - ServiceManagerWeb Workers
Tareas de mantenimiento del sistema
"""
from celery import current_task
from datetime import datetime, timedelta
from typing import Dict, Any, List
import os
import structlog
from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
settings = get_settings()
logger = get_logger(__name__)
@celery_app.task(bind=True)
def health_check(self) -> Dict[str, Any]:
"""
Worker health check task.
Returns basic health information about the worker.
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
try:
current_time = datetime.utcnow()
# Basic health checks
health_data = {
"status": "healthy",
"timestamp": current_time.isoformat(),
"worker_id": self.request.hostname,
"task_id": self.request.id,
"environment": settings.ENVIRONMENT,
"checks": {
"redis": "unknown", # TODO: Check Redis connectivity
"database": "unknown", # TODO: Check database connectivity
"disk_space": "unknown", # TODO: Check disk space
"memory": "unknown" # TODO: Check memory usage
}
}
task_logger.info("Worker health check completed", status="healthy")
return health_data
except Exception as exc:
task_logger.error(
"Worker health check failed",
error=str(exc),
exc_info=True
)
return {
"status": "unhealthy",
"timestamp": datetime.utcnow().isoformat(),
"error": str(exc)
}
@celery_app.task(
bind=True,
time_limit=1800, # 30 minutes
soft_time_limit=1500 # 25 minutes
)
def cleanup_old_logs(self) -> Dict[str, Any]:
"""
Clean up old log files and database records.
Removes:
- Log files older than LOG_RETENTION_DAYS
- Old notification logs
- Old audit logs (if configured)
- Temp files
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting cleanup of old logs")
try:
current_time = datetime.utcnow()
cutoff_date = current_time - timedelta(days=settings.LOG_RETENTION_DAYS)
cleanup_results = {
"started_at": current_time.isoformat(),
"cutoff_date": cutoff_date.isoformat(),
"files_removed": 0,
"bytes_freed": 0,
"database_records_removed": 0,
"errors": []
}
# TODO: Implement actual file cleanup
# For now, simulate cleanup
# Clean up log files
log_dir = "/app/logs"
if os.path.exists(log_dir):
for filename in os.listdir(log_dir):
filepath = os.path.join(log_dir, filename)
if os.path.isfile(filepath):
file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
if file_mtime < cutoff_date and filename.endswith('.log'):
try:
file_size = os.path.getsize(filepath)
os.remove(filepath)
cleanup_results["files_removed"] += 1
cleanup_results["bytes_freed"] += file_size
task_logger.info(f"Removed old log file", filename=filename)
except Exception as e:
cleanup_results["errors"].append(f"Failed to remove {filename}: {str(e)}")
# TODO: Clean up database records
# - Old notification_logs
# - Old audit_logs (with retention policy)
# - Expired refresh_tokens
# - Old file attachments (if configured)
task_logger.info(
"Cleanup completed",
files_removed=cleanup_results["files_removed"],
bytes_freed=cleanup_results["bytes_freed"],
errors=len(cleanup_results["errors"])
)
return cleanup_results
except Exception as exc:
task_logger.error(
"Cleanup task failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=3600, # 1 hour
soft_time_limit=3300 # 55 minutes
)
def generate_weekly_reports(self) -> Dict[str, Any]:
"""
Generate weekly reports for all tenants.
Creates:
- SLA performance reports
- Ticket volume reports
- Agent performance reports
- Customer satisfaction reports
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting weekly reports generation")
try:
current_time = datetime.utcnow()
week_start = current_time - timedelta(days=7)
report_results = {
"generated_at": current_time.isoformat(),
"period_start": week_start.isoformat(),
"period_end": current_time.isoformat(),
"reports_generated": [],
"errors": []
}
# TODO: Get list of active tenants from database
mock_tenants = [
{"id": "tenant-1", "name": "Aduanasoft Demo", "slug": "aduanasoft-demo"}
]
for tenant in mock_tenants:
try:
task_logger.info(
"Generating report for tenant",
tenant_id=tenant["id"],
tenant_name=tenant["name"]
)
# TODO: Generate actual reports
# For now, simulate report generation
report_data = {
"tenant_id": tenant["id"],
"tenant_name": tenant["name"],
"period_start": week_start.isoformat(),
"period_end": current_time.isoformat(),
"metrics": {
"tickets_created": 25,
"tickets_resolved": 23,
"avg_response_time_hours": 2.1,
"avg_resolution_time_hours": 18.5,
"sla_response_met_percentage": 92.0,
"sla_resolution_met_percentage": 87.0,
"customer_satisfaction_avg": 4.2
}
}
report_results["reports_generated"].append(report_data)
# TODO: Store report in database
# TODO: Send report email to admins
# Update task progress
current_task.update_state(
state='PROGRESS',
meta={
'current': len(report_results["reports_generated"]),
'total': len(mock_tenants)
}
)
except Exception as e:
error_msg = f"Failed to generate report for tenant {tenant['id']}: {str(e)}"
report_results["errors"].append(error_msg)
task_logger.error(
"Report generation failed for tenant",
tenant_id=tenant["id"],
error=str(e)
)
task_logger.info(
"Weekly reports generation completed",
reports_generated=len(report_results["reports_generated"]),
errors=len(report_results["errors"])
)
return report_results
except Exception as exc:
task_logger.error(
"Weekly reports generation failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=900, # 15 minutes
soft_time_limit=780 # 13 minutes
)
def cleanup_temp_files(self) -> Dict[str, Any]:
"""
Clean up temporary files and orphaned uploads.
Removes:
- Temp upload files older than 24 hours
- Orphaned attachment files (no DB reference)
- Processing artifacts
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting temp files cleanup")
try:
current_time = datetime.utcnow()
cutoff_date = current_time - timedelta(hours=24)
cleanup_results = {
"started_at": current_time.isoformat(),
"temp_files_removed": 0,
"orphaned_files_removed": 0,
"bytes_freed": 0,
"errors": []
}
# Clean up temp directory
temp_dirs = ["/tmp", "/app/temp", f"{settings.UPLOAD_PATH}/temp"]
for temp_dir in temp_dirs:
if os.path.exists(temp_dir):
for filename in os.listdir(temp_dir):
filepath = os.path.join(temp_dir, filename)
if os.path.isfile(filepath):
try:
file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
if file_mtime < cutoff_date:
file_size = os.path.getsize(filepath)
os.remove(filepath)
cleanup_results["temp_files_removed"] += 1
cleanup_results["bytes_freed"] += file_size
except Exception as e:
cleanup_results["errors"].append(f"Failed to remove temp file {filepath}: {str(e)}")
# TODO: Check for orphaned files in uploads directory
# - Query database for all attachment file_paths
# - Compare with actual files in upload directory
# - Remove orphaned files
task_logger.info(
"Temp files cleanup completed",
temp_files_removed=cleanup_results["temp_files_removed"],
orphaned_files_removed=cleanup_results["orphaned_files_removed"],
bytes_freed=cleanup_results["bytes_freed"]
)
return cleanup_results
except Exception as exc:
task_logger.error(
"Temp files cleanup failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def database_maintenance(self) -> Dict[str, Any]:
"""
Perform database maintenance tasks.
- VACUUM and ANALYZE tables
- Update statistics
- Check for slow queries
- Optimize indices if needed
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting database maintenance")
try:
# TODO: Implement database maintenance
# For now, return placeholder results
maintenance_results = {
"started_at": datetime.utcnow().isoformat(),
"tables_analyzed": 0,
"indices_optimized": 0,
"slow_queries_found": 0,
"space_reclaimed_mb": 0
}
task_logger.info("Database maintenance completed (placeholder)")
return maintenance_results
except Exception as exc:
task_logger.error(
"Database maintenance failed",
error=str(exc),
exc_info=True
)
raise

View File

@@ -0,0 +1,469 @@
"""
Notification Tasks - ServiceManagerWeb Workers
Tareas para notificaciones y comunicaciones
"""
from celery import current_task
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
import structlog
from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
from app.tasks.email_tasks import send_templated_email_task, send_bulk_email_task
settings = get_settings()
logger = get_logger(__name__)
@celery_app.task(
bind=True,
time_limit=900, # 15 minutes
soft_time_limit=780 # 13 minutes
)
def send_daily_digest(self) -> Dict[str, Any]:
"""
Send daily digest emails to agents and managers.
Includes:
- New tickets assigned
- SLA warnings
- Performance summary
- Pending tasks
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting daily digest generation")
if not settings.DIGEST_ENABLED:
task_logger.info("Daily digest disabled, skipping")
return {"status": "disabled"}
try:
current_time = datetime.utcnow()
yesterday = current_time - timedelta(days=1)
digest_results = {
"generated_at": current_time.isoformat(),
"period_start": yesterday.isoformat(),
"period_end": current_time.isoformat(),
"digests_sent": 0,
"errors": []
}
# TODO: Get active agents and managers from database
mock_recipients = [
{
"user_id": "user-1",
"email": "agent1@example.com",
"name": "Agent One",
"role": "AGENT",
"tenant_id": "tenant-1"
},
{
"user_id": "user-2",
"email": "manager@example.com",
"name": "Support Manager",
"role": "SUPPORT_MANAGER",
"tenant_id": "tenant-1"
}
]
for recipient in mock_recipients:
try:
task_logger.info(
"Generating digest for user",
user_id=recipient["user_id"],
email=recipient["email"],
role=recipient["role"]
)
# TODO: Generate actual digest data from database
digest_data = generate_digest_data(
recipient["user_id"],
recipient["role"],
recipient["tenant_id"],
yesterday,
current_time
)
# Send digest email
send_templated_email_task.apply_async(kwargs={
"to_email": recipient["email"],
"template_name": "daily_digest",
"context": {
"user_name": recipient["name"],
"role": recipient["role"],
"date": current_time.strftime("%Y-%m-%d"),
**digest_data
},
"tenant_id": recipient["tenant_id"],
"correlation_id": self.request.id
})
digest_results["digests_sent"] += 1
except Exception as e:
error_msg = f"Failed to send digest to {recipient['email']}: {str(e)}"
digest_results["errors"].append(error_msg)
task_logger.error(
"Digest generation failed for user",
user_id=recipient["user_id"],
error=str(e)
)
task_logger.info(
"Daily digest generation completed",
digests_sent=digest_results["digests_sent"],
errors=len(digest_results["errors"])
)
return digest_results
except Exception as exc:
task_logger.error(
"Daily digest generation failed",
error=str(exc),
exc_info=True
)
raise
def generate_digest_data(
user_id: str,
role: str,
tenant_id: str,
period_start: datetime,
period_end: datetime
) -> Dict[str, Any]:
"""
Generate digest data for a specific user.
Args:
user_id: User ID
role: User role
tenant_id: Tenant ID
period_start: Start of digest period
period_end: End of digest period
Returns:
Dict with digest data
"""
# TODO: Implement actual database queries
# For now, return mock data
base_data = {
"summary": {
"new_tickets": 5,
"resolved_tickets": 7,
"pending_tickets": 12,
"overdue_tickets": 2
},
"sla_status": {
"response_sla_met": 8,
"response_sla_missed": 1,
"resolution_sla_met": 6,
"resolution_sla_missed": 2
}
}
if role == "AGENT":
base_data.update({
"assigned_tickets": [
{
"ticket_number": "TKT-2024-000001",
"subject": "Problema de conexión",
"priority": "HIGH",
"created_at": "2024-01-15T10:00:00Z",
"sla_due": "2024-01-15T12:00:00Z"
}
],
"urgent_tickets": 1,
"performance": {
"avg_response_time_hours": 1.5,
"avg_resolution_time_hours": 18.2,
"customer_satisfaction": 4.3
}
})
elif role in ["SUPPORT_MANAGER", "ADMIN"]:
base_data.update({
"team_summary": {
"total_agents": 5,
"active_agents": 4,
"avg_load_per_agent": 6.2
},
"escalations": [
{
"ticket_number": "TKT-2024-000002",
"reason": "SLA violation",
"assigned_to": "agent1@example.com"
}
],
"trends": {
"ticket_volume_change": "+12%",
"resolution_time_change": "-5%"
}
})
return base_data
@celery_app.task(
bind=True,
time_limit=600,
soft_time_limit=540
)
def send_ticket_notifications(
self,
ticket_id: str,
event_type: str,
tenant_id: str,
context: Dict[str, Any],
correlation_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send ticket-related notifications.
Args:
ticket_id: Ticket ID
event_type: Type of event (created, assigned, updated, resolved, etc.)
tenant_id: Tenant ID
context: Context data for notifications
correlation_id: Correlation ID
Returns:
Dict with notification results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
ticket_id=ticket_id,
event_type=event_type,
tenant_id=tenant_id,
correlation_id=correlation_id
)
task_logger.info("Starting ticket notifications")
try:
notification_results = {
"ticket_id": ticket_id,
"event_type": event_type,
"notifications_sent": 0,
"notifications": []
}
# Determine who should receive notifications based on event type
recipients = get_notification_recipients(ticket_id, event_type, tenant_id)
for recipient in recipients:
try:
template_name = f"ticket_{event_type}"
# Send notification
result = send_templated_email_task.apply_async(kwargs={
"to_email": recipient["email"],
"template_name": template_name,
"context": {
**context,
"recipient_name": recipient["name"],
"recipient_role": recipient["role"]
},
"tenant_id": tenant_id,
"correlation_id": correlation_id or self.request.id
}).get()
notification_results["notifications"].append({
"recipient": recipient["email"],
"template": template_name,
"success": result.get("success", False),
"error": result.get("error")
})
if result.get("success"):
notification_results["notifications_sent"] += 1
except Exception as e:
task_logger.error(
"Failed to send notification",
recipient_email=recipient["email"],
error=str(e)
)
notification_results["notifications"].append({
"recipient": recipient["email"],
"success": False,
"error": str(e)
})
task_logger.info(
"Ticket notifications completed",
notifications_sent=notification_results["notifications_sent"],
total_recipients=len(recipients)
)
return notification_results
except Exception as exc:
task_logger.error(
"Ticket notifications failed",
error=str(exc),
exc_info=True
)
raise
def get_notification_recipients(
ticket_id: str,
event_type: str,
tenant_id: str
) -> List[Dict[str, Any]]:
"""
Get list of users who should receive notifications for a ticket event.
Args:
ticket_id: Ticket ID
event_type: Event type
tenant_id: Tenant ID
Returns:
List of recipient dicts
"""
# TODO: Implement actual database queries
# For now, return mock recipients based on event type
recipients = []
if event_type == "created":
# Notify assigned agent (if any) and customer
recipients = [
{"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"},
{"email": "agent@example.com", "name": "Agent", "role": "AGENT"}
]
elif event_type == "assigned":
# Notify assigned agent and customer
recipients = [
{"email": "agent@example.com", "name": "Assigned Agent", "role": "AGENT"},
{"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"}
]
elif event_type == "updated":
# Notify all participants
recipients = [
{"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"},
{"email": "agent@example.com", "name": "Agent", "role": "AGENT"}
]
elif event_type == "resolved":
# Notify customer for feedback
recipients = [
{"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"}
]
elif event_type == "escalated":
# Notify manager
recipients = [
{"email": "manager@example.com", "name": "Manager", "role": "SUPPORT_MANAGER"}
]
return recipients
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def send_system_alert(
self,
alert_type: str,
message: str,
severity: str = "INFO",
tenant_id: Optional[str] = None,
context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Send system alert to administrators.
Args:
alert_type: Type of alert (system_error, sla_violation, etc.)
message: Alert message
severity: Alert severity (INFO, WARNING, ERROR, CRITICAL)
tenant_id: Optional tenant ID
context: Additional context data
Returns:
Dict with alert results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
alert_type=alert_type,
severity=severity,
tenant_id=tenant_id
)
task_logger.info("Sending system alert", message=message)
try:
# TODO: Get administrators from configuration/database
admin_emails = ["admin@example.com", "alerts@example.com"]
alert_context = {
"alert_type": alert_type,
"message": message,
"severity": severity,
"timestamp": datetime.utcnow().isoformat(),
"environment": settings.ENVIRONMENT,
"tenant_id": tenant_id,
**(context or {})
}
notifications_sent = 0
for admin_email in admin_emails:
try:
send_templated_email_task.apply_async(kwargs={
"to_email": admin_email,
"template_name": "system_alert",
"context": alert_context,
"tenant_id": tenant_id,
"correlation_id": self.request.id
})
notifications_sent += 1
except Exception as e:
task_logger.error(
"Failed to send alert to admin",
admin_email=admin_email,
error=str(e)
)
task_logger.info(
"System alert sent",
notifications_sent=notifications_sent,
total_admins=len(admin_emails)
)
return {
"alert_type": alert_type,
"message": message,
"severity": severity,
"notifications_sent": notifications_sent,
"sent_at": datetime.utcnow().isoformat()
}
except Exception as exc:
task_logger.error(
"System alert failed",
error=str(exc),
exc_info=True
)
raise

View File

@@ -0,0 +1,327 @@
"""
SLA Tasks - ServiceManagerWeb Workers
Tareas para monitoreo y gestión de SLAs
"""
from celery import current_task
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import structlog
from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
from app.tasks.email_tasks import send_templated_email_task
settings = get_settings()
logger = get_logger(__name__)
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def check_sla_violations(self) -> Dict[str, Any]:
"""
Check for SLA violations and send alerts.
This task runs every 5 minutes to check for:
- Response SLA violations
- Resolution SLA violations
- SLA warnings (approaching deadline)
Returns:
Dict with check results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting SLA violations check")
if not settings.SLA_CHECK_ENABLED:
task_logger.info("SLA check disabled, skipping")
return {"status": "disabled"}
try:
current_time = datetime.utcnow()
results = {
"checked_at": current_time.isoformat(),
"response_violations": [],
"resolution_violations": [],
"warnings": [],
"notifications_sent": 0
}
# TODO: Implement actual database queries
# For now, simulate some checks
# Mock violations for development
mock_violations = [
{
"ticket_id": "mock-ticket-1",
"ticket_number": "TKT-2024-000001",
"subject": "Problema urgente de conexión",
"priority": "HIGH",
"sla_type": "response",
"due_at": (current_time - timedelta(minutes=30)).isoformat(),
"assigned_to_email": "agent@example.com",
"created_by_email": "cliente@example.com",
"tenant_id": "mock-tenant-1"
}
]
# Process violations
for violation in mock_violations:
task_logger.info(
"Processing SLA violation",
ticket_id=violation["ticket_id"],
sla_type=violation["sla_type"]
)
if violation["sla_type"] == "response":
results["response_violations"].append(violation)
# Send notification to assigned agent
if violation["assigned_to_email"]:
send_templated_email_task.apply_async(kwargs={
"to_email": violation["assigned_to_email"],
"template_name": "sla_response_violation",
"context": {
"ticket_number": violation["ticket_number"],
"subject": violation["subject"],
"priority": violation["priority"],
"due_at": violation["due_at"],
"ticket_url": f"https://admin.servicemanager.local/tickets/{violation['ticket_id']}"
},
"tenant_id": violation["tenant_id"],
"correlation_id": self.request.id
})
results["notifications_sent"] += 1
elif violation["sla_type"] == "resolution":
results["resolution_violations"].append(violation)
# Send escalation notification
send_templated_email_task.apply_async(kwargs={
"to_email": "manager@example.com", # TODO: Get from tenant config
"template_name": "sla_resolution_violation",
"context": {
"ticket_number": violation["ticket_number"],
"subject": violation["subject"],
"priority": violation["priority"],
"assigned_to": violation["assigned_to_email"],
"ticket_url": f"https://admin.servicemanager.local/tickets/{violation['ticket_id']}"
},
"tenant_id": violation["tenant_id"],
"correlation_id": self.request.id
})
results["notifications_sent"] += 1
# TODO: Check for SLA warnings (approaching deadline)
task_logger.info(
"SLA violations check completed",
response_violations=len(results["response_violations"]),
resolution_violations=len(results["resolution_violations"]),
warnings=len(results["warnings"]),
notifications_sent=results["notifications_sent"]
)
return results
except Exception as exc:
task_logger.error(
"SLA violations check failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=600,
soft_time_limit=540
)
def calculate_sla_metrics(self, tenant_id: str, date_from: str, date_to: str) -> Dict[str, Any]:
"""
Calculate SLA metrics for a tenant and date range.
Args:
tenant_id: Tenant ID
date_from: Start date (ISO format)
date_to: End date (ISO format)
Returns:
Dict with SLA metrics
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id
)
task_logger.info(
"Starting SLA metrics calculation",
date_from=date_from,
date_to=date_to
)
try:
# TODO: Implement actual database queries
# For now, return mock metrics
metrics = {
"tenant_id": tenant_id,
"date_from": date_from,
"date_to": date_to,
"calculated_at": datetime.utcnow().isoformat(),
"response_sla": {
"target_hours": 2,
"met_count": 45,
"total_count": 50,
"percentage": 90.0,
"avg_response_time_hours": 1.8
},
"resolution_sla": {
"target_hours": 24,
"met_count": 42,
"total_count": 48,
"percentage": 87.5,
"avg_resolution_time_hours": 22.5
},
"by_priority": {
"LOW": {
"response_sla_percentage": 95.0,
"resolution_sla_percentage": 90.0
},
"MEDIUM": {
"response_sla_percentage": 88.0,
"resolution_sla_percentage": 85.0
},
"HIGH": {
"response_sla_percentage": 92.0,
"resolution_sla_percentage": 88.0
},
"URGENT": {
"response_sla_percentage": 85.0,
"resolution_sla_percentage": 80.0
}
},
"trends": {
"response_sla_trend": "+2.5%",
"resolution_sla_trend": "-1.2%"
}
}
task_logger.info(
"SLA metrics calculation completed",
response_sla_percentage=metrics["response_sla"]["percentage"],
resolution_sla_percentage=metrics["resolution_sla"]["percentage"]
)
return metrics
except Exception as exc:
task_logger.error(
"SLA metrics calculation failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def send_sla_warnings(self, tenant_id: Optional[str] = None) -> Dict[str, Any]:
"""
Send SLA warning notifications for tickets approaching deadline.
Args:
tenant_id: Optional tenant ID to filter by
Returns:
Dict with warning results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id
)
task_logger.info("Starting SLA warnings check")
try:
current_time = datetime.utcnow()
warning_threshold = settings.SLA_WARNING_THRESHOLD # 80% of SLA time
# TODO: Query database for tickets approaching SLA deadlines
# Mock warnings
warnings = [
{
"ticket_id": "mock-ticket-2",
"ticket_number": "TKT-2024-000002",
"subject": "Consulta técnica",
"priority": "MEDIUM",
"sla_type": "response",
"due_at": (current_time + timedelta(minutes=30)).isoformat(),
"time_remaining_percent": 15.0,
"assigned_to_email": "agent@example.com",
"tenant_id": "mock-tenant-1"
}
]
notifications_sent = 0
for warning in warnings:
if warning["time_remaining_percent"] <= (1 - warning_threshold) * 100:
task_logger.info(
"Sending SLA warning",
ticket_id=warning["ticket_id"],
time_remaining_percent=warning["time_remaining_percent"]
)
send_templated_email_task.apply_async(kwargs={
"to_email": warning["assigned_to_email"],
"template_name": "sla_warning",
"context": {
"ticket_number": warning["ticket_number"],
"subject": warning["subject"],
"priority": warning["priority"],
"sla_type": warning["sla_type"],
"due_at": warning["due_at"],
"time_remaining_percent": warning["time_remaining_percent"],
"ticket_url": f"https://admin.servicemanager.local/tickets/{warning['ticket_id']}"
},
"tenant_id": warning["tenant_id"],
"correlation_id": self.request.id
})
notifications_sent += 1
task_logger.info(
"SLA warnings check completed",
warnings_found=len(warnings),
notifications_sent=notifications_sent
)
return {
"warnings_found": len(warnings),
"notifications_sent": notifications_sent,
"warnings": warnings
}
except Exception as exc:
task_logger.error(
"SLA warnings check failed",
error=str(exc),
exc_info=True
)
raise

74
workers/requirements.txt Normal file
View File

@@ -0,0 +1,74 @@
# Celery Workers Requirements - ServiceManagerWeb
# Dependencias para workers asíncronos
# ===================================
# CORE CELERY
# ===================================
celery==5.3.4
redis==5.0.1
# ===================================
# DATABASE (shared with backend)
# ===================================
sqlalchemy==2.0.23
asyncpg==0.29.0
psycopg2-binary==2.9.9
alembic==1.13.0
# ===================================
# CONFIGURATION & VALIDATION
# ===================================
pydantic==2.5.0
pydantic-settings==2.1.0
# ===================================
# EMAIL PROCESSING
# ===================================
email-validator==2.1.0
jinja2==3.1.2
aiosmtplib==3.0.1 # Async SMTP client
premailer==3.10.0 # CSS inlining for emails
beautifulsoup4==4.12.2 # HTML parsing
# ===================================
# FILE PROCESSING
# ===================================
python-magic==0.4.27
pillow==10.1.0
pypdf==3.17.1 # PDF processing
openpyxl==3.1.2 # Excel processing
# ===================================
# HTTP CLIENTS
# ===================================
httpx==0.25.2
aiofiles==23.2.1
# ===================================
# UTILITIES
# ===================================
python-dateutil==2.8.2
pytz==2023.3
slugify==0.0.1
# ===================================
# LOGGING & MONITORING
# ===================================
structlog==23.2.0
prometheus-client==0.19.0
sentry-sdk==1.38.0
# ===================================
# DEVELOPMENT
# ===================================
pytest==7.4.3
pytest-asyncio==0.21.1
pytest-celery==0.0.0a1
faker==20.1.0
# ===================================
# CODE QUALITY
# ===================================
ruff==0.1.7
black==23.11.0
mypy==1.7.1