Mudanza de repo
This commit is contained in:
0
api/logger/__init__.py
Normal file
0
api/logger/__init__.py
Normal file
149
api/logger/admin.py
Normal file
149
api/logger/admin.py
Normal 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
6
api/logger/apps.py
Normal 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
91
api/logger/middleware.py
Normal 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
|
||||
73
api/logger/migrations/0001_initial.py
Normal file
73
api/logger/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
api/logger/migrations/__init__.py
Normal file
0
api/logger/migrations/__init__.py
Normal file
103
api/logger/mixins.py
Normal file
103
api/logger/mixins.py
Normal 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
89
api/logger/models.py
Normal 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
29
api/logger/serializers.py
Normal 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
3
api/logger/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
12
api/logger/urls.py
Normal file
12
api/logger/urls.py
Normal 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
98
api/logger/utils.py
Normal 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
92
api/logger/views.py
Normal 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)
|
||||
Reference in New Issue
Block a user