Mudanza de repo

This commit is contained in:
2025-09-22 18:43:29 -06:00
parent 26fe36ca52
commit d11d543bdc
193 changed files with 10998 additions and 0 deletions

0
api/logger/__init__.py Normal file
View File

149
api/logger/admin.py Normal file
View File

@@ -0,0 +1,149 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .models import RequestLog, UserActivity, ErrorLog
import json
from config.settings import SITE_URL
class ReadOnlyAdminMixin:
"""Mixin para hacer que los modelos sean solo lectura en el admin."""
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
@admin.register(RequestLog)
class RequestLogAdmin(ReadOnlyAdminMixin, admin.ModelAdmin):
list_display = [
'timestamp', 'user_display', 'method', 'path', 'status_code',
'response_time', 'ip_address'
]
list_filter = [
'method', 'status_code', 'timestamp', 'user_agent'
]
search_fields = [
'path', 'ip_address', 'user__username', 'user__email'
]
readonly_fields = [
'timestamp', 'user', 'method', 'path', 'query_params_display',
'status_code', 'response_time', 'ip_address', 'user_agent',
'body_display', 'referer'
]
ordering = ['-timestamp']
date_hierarchy = 'timestamp'
list_per_page = 50
def user_display(self, obj):
if obj.user:
return f"{obj.user.username} ({obj.user.email})"
return "Anónimo"
user_display.short_description = "Usuario"
def query_params_display(self, obj):
if obj.query_params:
try:
params = json.loads(obj.query_params) if isinstance(obj.query_params, str) else obj.query_params
formatted = json.dumps(params, indent=2, ensure_ascii=False)
return format_html('<pre>{}</pre>', formatted)
except:
return obj.query_params
return "Sin parámetros"
query_params_display.short_description = "Parámetros de consulta"
def body_display(self, obj):
if obj.body:
try:
# Intentar formatear como JSON si es posible
body_data = json.loads(obj.body) if isinstance(obj.body, str) else obj.body
formatted = json.dumps(body_data, indent=2, ensure_ascii=False)
return format_html('<pre style="max-height: 200px; overflow-y: auto;">{}</pre>', formatted)
except:
# Si no es JSON válido, mostrar como texto
return format_html('<pre style="max-height: 200px; overflow-y: auto;">{}</pre>', obj.body[:1000])
return "Sin body"
body_display.short_description = "Cuerpo del request"
@admin.register(UserActivity)
class UserActivityAdmin(admin.ModelAdmin):
list_display = [
'timestamp', 'user_display', 'action', 'object_type',
'object_id', 'ip_address'
]
list_filter = [
'action', 'object_type', 'timestamp'
]
search_fields = [
'user__username', 'user__email', 'action', 'object_type',
'object_id', 'ip_address', 'description'
]
readonly_fields = [
'timestamp', 'user', 'action', 'object_type', 'object_id',
'description', 'ip_address'
]
ordering = ['-timestamp']
date_hierarchy = 'timestamp'
list_per_page = 50
def user_display(self, obj):
if obj.user:
return f"{obj.user.username} ({obj.user.email})"
return "Sistema"
user_display.short_description = "Usuario"
@admin.register(ErrorLog)
class ErrorLogAdmin(ReadOnlyAdminMixin, admin.ModelAdmin):
list_display = [
'timestamp', 'user_display', 'level', 'message_short',
'request_path'
]
list_filter = [
'level', 'timestamp'
]
search_fields = [
'user__username', 'user__email', 'level', 'message',
'request_path', 'traceback'
]
readonly_fields = [
'timestamp', 'user', 'level', 'message', 'traceback_display',
'request_path', 'ip_address'
]
ordering = ['-timestamp']
date_hierarchy = 'timestamp'
list_per_page = 25
def user_display(self, obj):
if obj.user:
return f"{obj.user.username} ({obj.user.email})"
return "Sistema/Anónimo"
user_display.short_description = "Usuario"
def message_short(self, obj):
if obj.message and len(obj.message) > 100:
return f"{obj.message[:100]}..."
return obj.message or "Sin mensaje"
message_short.short_description = "Mensaje"
def traceback_display(self, obj):
if obj.traceback:
return format_html(
'<pre style="max-height: 400px; overflow-y: auto; background: #f8f8f8; padding: 10px; border: 1px solid #ddd;">{}</pre>',
obj.traceback
)
return "Sin traceback"
traceback_display.short_description = "Stack trace"
# Personalización del admin site
admin.site.site_header = "EFC V2 "
admin.site.site_title = "EFC V2"
admin.site.index_title = "Administración del Sistema"
admin.site.site_url = SITE_URL

6
api/logger/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class LoggerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.logger'

91
api/logger/middleware.py Normal file
View File

@@ -0,0 +1,91 @@
import time
import json
import logging
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth.models import AnonymousUser
from .models import RequestLog, ErrorLog
logger = logging.getLogger('django')
class RequestLoggingMiddleware(MiddlewareMixin):
def process_request(self, request):
request.start_time = time.time()
return None
def process_response(self, request, response):
# Calcular tiempo de respuesta
response_time = (time.time() - getattr(request, 'start_time', 0)) * 1000
# Obtener información del usuario
user = request.user if not isinstance(request.user, AnonymousUser) else None
# Obtener IP del cliente
ip_address = self.get_client_ip(request)
# Obtener query parameters
query_params = dict(request.GET) if request.GET else {}
# Obtener body de la request (solo para POST, PUT, PATCH)
body = ""
if request.method in ['POST', 'PUT', 'PATCH']:
try:
if hasattr(request, 'body'):
body = request.body.decode('utf-8')[:1000] # Limitar a 1000 caracteres
except Exception:
body = "Could not decode body"
# Crear log de la request
try:
RequestLog.objects.create(
user=user,
ip_address=ip_address,
user_agent=request.META.get('HTTP_USER_AGENT', '')[:500],
method=request.method,
path=request.path,
query_params=json.dumps(query_params),
body=body,
status_code=response.status_code,
response_time=response_time,
referer=request.META.get('HTTP_REFERER', '')
)
except Exception as e:
logger.error(f"Error logging request: {e}")
return response
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class ErrorLoggingMiddleware(MiddlewareMixin):
def process_exception(self, request, exception):
import traceback
user = request.user if not isinstance(request.user, AnonymousUser) else None
ip_address = self.get_client_ip(request)
try:
ErrorLog.objects.create(
level='ERROR',
message=str(exception),
traceback=traceback.format_exc(),
user=user,
ip_address=ip_address,
request_path=request.path
)
except Exception as e:
logger.error(f"Error logging exception: {e}")
return None
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip

View File

@@ -0,0 +1,73 @@
# Generated by Django 5.2.3 on 2025-07-14 16:14
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ErrorLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('level', models.CharField(choices=[('DEBUG', 'Debug'), ('INFO', 'Info'), ('WARNING', 'Warning'), ('ERROR', 'Error'), ('CRITICAL', 'Critical')], max_length=10)),
('message', models.TextField()),
('traceback', models.TextField(blank=True)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('request_path', models.URLField(blank=True, max_length=500)),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'logger_error_log',
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='RequestLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ip_address', models.GenericIPAddressField()),
('user_agent', models.TextField(blank=True)),
('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PUT', 'PUT'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('OPTIONS', 'OPTIONS'), ('HEAD', 'HEAD')], max_length=10)),
('path', models.URLField(max_length=500)),
('query_params', models.TextField(blank=True)),
('body', models.TextField(blank=True)),
('status_code', models.IntegerField()),
('response_time', models.FloatField()),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('referer', models.URLField(blank=True, max_length=500)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'logger_request_log',
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='UserActivity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.CharField(choices=[('login', 'Login'), ('logout', 'Logout'), ('create', 'Create'), ('update', 'Update'), ('delete', 'Delete'), ('view', 'View'), ('search', 'Search'), ('export', 'Export'), ('import', 'Import')], max_length=20)),
('object_type', models.CharField(blank=True, max_length=100)),
('object_id', models.CharField(blank=True, max_length=100)),
('description', models.TextField(blank=True)),
('ip_address', models.GenericIPAddressField()),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'logger_user_activity',
'ordering': ['-timestamp'],
},
),
]

View File

103
api/logger/mixins.py Normal file
View File

@@ -0,0 +1,103 @@
from .utils import log_user_activity
class LoggingMixin:
"""
Mixin para añadir logging automático a ViewSets
"""
log_actions = True
log_object_type = None
def get_log_object_type(self):
"""Obtiene el tipo de objeto del modelo del ViewSet"""
if self.log_object_type:
return self.log_object_type
if hasattr(self, 'queryset') and self.queryset is not None:
return self.queryset.model.__name__
if hasattr(self, 'model') and self.model is not None:
return self.model.__name__
return self.__class__.__name__.replace('ViewSet', '')
def perform_create(self, serializer):
"""Override para loggear creaciones"""
instance = serializer.save()
if self.log_actions and self.request.user.is_authenticated:
log_user_activity(
user=self.request.user,
action='create',
object_type=self.get_log_object_type(),
object_id=instance.pk,
description=f'Creado {self.get_log_object_type()} {instance.pk}',
request=self.request
)
return instance
def perform_update(self, serializer):
"""Override para loggear actualizaciones"""
instance = serializer.save()
if self.log_actions and self.request.user.is_authenticated:
log_user_activity(
user=self.request.user,
action='update',
object_type=self.get_log_object_type(),
object_id=instance.pk,
description=f'Actualizado {self.get_log_object_type()} {instance.pk}',
request=self.request
)
return instance
def perform_destroy(self, instance):
"""Override para loggear eliminaciones"""
object_id = instance.pk
object_type = self.get_log_object_type()
instance.delete()
if self.log_actions and self.request.user.is_authenticated:
log_user_activity(
user=self.request.user,
action='delete',
object_type=object_type,
object_id=object_id,
description=f'Eliminado {object_type} {object_id}',
request=self.request
)
def retrieve(self, request, *args, **kwargs):
"""Override para loggear visualizaciones de detalle"""
response = super().retrieve(request, *args, **kwargs)
if self.log_actions and request.user.is_authenticated:
instance = self.get_object()
log_user_activity(
user=request.user,
action='view',
object_type=self.get_log_object_type(),
object_id=instance.pk,
description=f'Visto detalle de {self.get_log_object_type()} {instance.pk}',
request=request
)
return response
def list(self, request, *args, **kwargs):
"""Override para loggear listados"""
response = super().list(request, *args, **kwargs)
if self.log_actions and request.user.is_authenticated:
log_user_activity(
user=request.user,
action='view',
object_type=self.get_log_object_type(),
object_id='',
description=f'Visto listado de {self.get_log_object_type()}',
request=request
)
return response

89
api/logger/models.py Normal file
View File

@@ -0,0 +1,89 @@
from django.db import models
from api.cuser.models import CustomUser as User # Asegúrate de que este es el modelo de usuario correcto
from django.utils import timezone
class RequestLog(models.Model):
METHODS = (
('GET', 'GET'),
('POST', 'POST'),
('PUT', 'PUT'),
('PATCH', 'PATCH'),
('DELETE', 'DELETE'),
('OPTIONS', 'OPTIONS'),
('HEAD', 'HEAD'),
)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
ip_address = models.GenericIPAddressField()
user_agent = models.TextField(blank=True)
method = models.CharField(max_length=10, choices=METHODS)
path = models.URLField(max_length=500)
query_params = models.TextField(blank=True)
body = models.TextField(blank=True)
status_code = models.IntegerField()
response_time = models.FloatField() # en milisegundos
timestamp = models.DateTimeField(default=timezone.now)
referer = models.URLField(max_length=500, blank=True)
class Meta:
db_table = 'logger_request_log'
ordering = ['-timestamp']
def __str__(self):
return f"{self.method} {self.path} - {self.status_code} ({self.timestamp})"
class UserActivity(models.Model):
ACTIONS = (
('login', 'Login'),
('logout', 'Logout'),
('create', 'Create'),
('update', 'Update'),
('delete', 'Delete'),
('view', 'View'),
('search', 'Search'),
('export', 'Export'),
('import', 'Import'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
action = models.CharField(max_length=20, choices=ACTIONS)
object_type = models.CharField(max_length=100, blank=True) # modelo afectado
object_id = models.CharField(max_length=100, blank=True) # ID del objeto
description = models.TextField(blank=True)
ip_address = models.GenericIPAddressField()
timestamp = models.DateTimeField(default=timezone.now)
class Meta:
db_table = 'logger_user_activity'
ordering = ['-timestamp']
def __str__(self):
return f"{self.user.username} - {self.action} ({self.timestamp})"
class ErrorLog(models.Model):
ERROR_LEVELS = (
('DEBUG', 'Debug'),
('INFO', 'Info'),
('WARNING', 'Warning'),
('ERROR', 'Error'),
('CRITICAL', 'Critical'),
)
level = models.CharField(max_length=10, choices=ERROR_LEVELS)
message = models.TextField()
traceback = models.TextField(blank=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
request_path = models.URLField(max_length=500, blank=True)
timestamp = models.DateTimeField(default=timezone.now)
class Meta:
db_table = 'logger_error_log'
ordering = ['-timestamp']
def __str__(self):
return f"{self.level}: {self.message[:50]}... ({self.timestamp})"

29
api/logger/serializers.py Normal file
View File

@@ -0,0 +1,29 @@
from rest_framework import serializers
from .models import RequestLog, UserActivity, ErrorLog
from api.cuser.models import CustomUser
class UserSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['id', 'username', 'first_name', 'last_name', 'email']
class RequestLogSerializer(serializers.ModelSerializer):
user = UserSimpleSerializer(read_only=True)
class Meta:
model = RequestLog
fields = '__all__'
class UserActivitySerializer(serializers.ModelSerializer):
user = UserSimpleSerializer(read_only=True)
class Meta:
model = UserActivity
fields = '__all__'
class ErrorLogSerializer(serializers.ModelSerializer):
user = UserSimpleSerializer(read_only=True)
class Meta:
model = ErrorLog
fields = '__all__'

3
api/logger/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
api/logger/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RequestLogViewSet, UserActivityViewSet, ErrorLogViewSet
router = DefaultRouter()
router.register(r'requests', RequestLogViewSet)
router.register(r'activities', UserActivityViewSet)
router.register(r'errors', ErrorLogViewSet)
urlpatterns = [
path('', include(router.urls)),
]

98
api/logger/utils.py Normal file
View File

@@ -0,0 +1,98 @@
from django.contrib.auth.models import User
from .models import UserActivity, ErrorLog
import logging
def get_client_ip(request):
"""Obtiene la IP real del cliente"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def log_user_activity(user, action, object_type='', object_id='', description='', request=None):
"""
Registra actividad del usuario
Args:
user: Usuario que realiza la acción
action: Tipo de acción (login, logout, create, update, delete, view, search, export, import)
object_type: Tipo de objeto afectado (opcional)
object_id: ID del objeto afectado (opcional)
description: Descripción adicional (opcional)
request: Request object para obtener IP (opcional)
"""
ip_address = '127.0.0.1'
if request:
ip_address = get_client_ip(request)
try:
UserActivity.objects.create(
user=user,
action=action,
object_type=object_type,
object_id=str(object_id) if object_id else '',
description=description,
ip_address=ip_address
)
except Exception as e:
logging.error(f"Error logging user activity: {e}")
def log_error(level, message, traceback='', user=None, request=None):
"""
Registra errores personalizados
Args:
level: Nivel del error (DEBUG, INFO, WARNING, ERROR, CRITICAL)
message: Mensaje del error
traceback: Traceback del error (opcional)
user: Usuario relacionado (opcional)
request: Request object (opcional)
"""
ip_address = None
request_path = ''
if request:
ip_address = get_client_ip(request)
request_path = request.path
try:
ErrorLog.objects.create(
level=level,
message=message,
traceback=traceback,
user=user,
ip_address=ip_address,
request_path=request_path
)
except Exception as e:
logging.error(f"Error logging custom error: {e}")
# Decorador para loggear automáticamente acciones
def log_action(action, object_type=''):
"""
Decorador para loggear automáticamente acciones en vistas
Usage:
@log_action('create', 'Pedimento')
def create_pedimento(request):
# tu código aquí
"""
def decorator(func):
def wrapper(request, *args, **kwargs):
result = func(request, *args, **kwargs)
if hasattr(request, 'user') and request.user.is_authenticated:
object_id = kwargs.get('pk', kwargs.get('id', ''))
log_user_activity(
user=request.user,
action=action,
object_type=object_type,
object_id=object_id,
request=request
)
return result
return wrapper
return decorator

92
api/logger/views.py Normal file
View File

@@ -0,0 +1,92 @@
from rest_framework import viewsets, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Q
from django.utils import timezone
from datetime import timedelta
from .models import RequestLog, UserActivity, ErrorLog
from .serializers import RequestLogSerializer, UserActivitySerializer, ErrorLogSerializer
from .utils import log_user_activity
from core.permissions import IsSuperUser
class RequestLogViewSet(viewsets.ReadOnlyModelViewSet):
queryset = RequestLog.objects.all()
serializer_class = RequestLogSerializer
permission_classes = [IsAuthenticated, IsSuperUser]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['method', 'status_code', 'user']
search_fields = ['path', 'ip_address', 'user_agent']
ordering_fields = ['timestamp', 'response_time', 'status_code']
ordering = ['-timestamp']
@action(detail=False, methods=['get'])
def statistics(self, request):
"""Estadísticas de requests"""
now = timezone.now()
today = now.date()
week_ago = now - timedelta(days=7)
stats = {
'total_requests': self.queryset.count(),
'today_requests': self.queryset.filter(timestamp__date=today).count(),
'week_requests': self.queryset.filter(timestamp__gte=week_ago).count(),
'methods': self.queryset.values('method').annotate(count=Count('method')),
'status_codes': self.queryset.values('status_code').annotate(count=Count('status_code')),
'top_endpoints': self.queryset.values('path').annotate(count=Count('path')).order_by('-count')[:10],
'avg_response_time': self.queryset.aggregate(avg_time=Count('response_time'))['avg_time']
}
return Response(stats)
class UserActivityViewSet(viewsets.ReadOnlyModelViewSet):
queryset = UserActivity.objects.all()
serializer_class = UserActivitySerializer
permission_classes = [IsAuthenticated, IsSuperUser]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['action', 'user', 'object_type']
search_fields = ['description', 'object_id']
ordering_fields = ['timestamp']
ordering = ['-timestamp']
def get_queryset(self):
# Check if user is authenticated first
if not self.request.user.is_authenticated:
return UserActivity.objects.none()
# Los usuarios normales solo ven su propia actividad
if self.request.user.is_staff:
return UserActivity.objects.all()
return UserActivity.objects.filter(user=self.request.user)
@action(detail=False, methods=['get'])
def my_activity(self, request):
"""Actividad del usuario actual"""
if not request.user.is_authenticated:
return Response({"error": "Usuario no autenticado"}, status=401)
activities = UserActivity.objects.filter(user=request.user)[:20]
serializer = self.get_serializer(activities, many=True)
return Response(serializer.data)
class ErrorLogViewSet(viewsets.ReadOnlyModelViewSet):
queryset = ErrorLog.objects.all()
serializer_class = ErrorLogSerializer
permission_classes = [IsAuthenticated, IsSuperUser]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['level', 'user']
search_fields = ['message', 'request_path']
ordering_fields = ['timestamp']
ordering = ['-timestamp']
@action(detail=False, methods=['get'])
def recent_errors(self, request):
"""Errores recientes (últimas 24 horas)"""
yesterday = timezone.now() - timedelta(days=1)
recent_errors = self.queryset.filter(timestamp__gte=yesterday)
serializer = self.get_serializer(recent_errors, many=True)
return Response(serializer.data)