├── .gitattributes ├── .gitignore ├── .pyup.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── request_viewer ├── __init__.py ├── admin.py ├── apps.py ├── conf.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_exceptionmodel_alter_logger_id.py │ └── __init__.py ├── models.py ├── templates │ └── request_viewer │ │ ├── exception.html │ │ ├── fragments │ │ ├── exception │ │ │ ├── modal.html │ │ │ ├── modal_content.html │ │ │ └── table.html │ │ ├── filter.html │ │ ├── modal.html │ │ ├── pagination.html │ │ └── request │ │ │ ├── filter.html │ │ │ ├── modal_content.html │ │ │ └── table.html │ │ └── request.html ├── templatetags │ ├── __init__.py │ └── request_view_tag.py ├── tests.py ├── urls.py ├── utils.py └── views.py ├── requirements.txt └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .idea/ 132 | 133 | ### macOS ### 134 | # General 135 | .DS_Store 136 | .AppleDouble 137 | .LSOverride 138 | 139 | # Icon must end with two \r 140 | Icon 141 | 142 | 143 | # Thumbnails 144 | ._* 145 | 146 | # Files that might appear in the root of a volume 147 | .DocumentRevisions-V100 148 | .fseventsd 149 | .Spotlight-V100 150 | .TemporaryItems 151 | .Trashes 152 | .VolumeIcon.icns 153 | .com.apple.timemachine.donotpresent 154 | 155 | # Directories potentially created on remote AFP share 156 | .AppleDB 157 | .AppleDesktop 158 | Network Trash Folder 159 | Temporary Items 160 | .apdisk 161 | /djexception/ 162 | /manage.py 163 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | schedule: '' 5 | update: insecure 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Akere Mukhtar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include request_viewer/templates * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Request Viewer 2 | 3 | 4 | Log and view requests and exceptions made on your Django App 5 | 6 | #### Updates 17th, January 2022 7 | 8 | - Adds Exception logger 9 | - Cleaned up the code 10 | 11 | ### Introduction 12 | 13 | 14 | Screenshot 2021-03-29 at 09 30 17 15 | 16 | 17 | Recently, [@ichtrojan](https://github.com/ichtrojan) and [@toniastro](https://github.com/toniastro) released [horus](https://github.com/ichtrojan/horus), a request logger and viewer for Go. Then I felt the need for something like that for the Django community. 18 | 19 | ### Installation 20 | 21 | Install using pip 22 | 23 | ```bash 24 | pip install django-request-viewer 25 | ``` 26 | 27 | ### Usage 28 | 29 | 30 | Add `'request-viewer'` to your `INSTALLED_APPS` in settings.py. 31 | 32 | INSTALLED_APPS = [ 33 | ... 34 | 'request_viewer', 35 | ... 36 | ] 37 | 38 | 39 | Add `'request_viewer.middleware.RequestViewerMiddleware'` to your MIDDLEWARE list in settings.py. 40 | 41 | MIDDLEWARE = [ 42 | ... 43 | 'request_viewer.middleware.RequestViewerMiddleware', 44 | ... 45 | ] 46 | 47 | ##### To log exceptions, add 48 | 49 | Add `'request_viewer.middleware.ExceptionMiddleware'` to your MIDDLEWARE list in settings.py. 50 | 51 | MIDDLEWARE = [ 52 | ... 53 | 'request_viewer.middleware.ExceptionMiddleware', 54 | ... 55 | ] 56 | 57 | Add `'request-viewer'` to your main urls.py 58 | 59 | urlpatterns = [ 60 | ... 61 | path('logs/', include('request_viewer.urls')) 62 | ... 63 | ] 64 | 65 | Run migrations, `python manage.py migrate request_viewer` 66 | 67 | **OPTIONAL** 68 | 69 | Add `REQUEST_VIEWER` dictionary to your settings.py. 70 | 71 | 72 | **LIVE_MONITORING**: Default: `True`, False to pause monitoring. 73 | 74 | **WHITELISTED_PATH**: Default: `[]`, This is a list of paths to be excluded when monitoring 75 | 76 | { 77 | "LIVE_MONITORING": True, 78 | "WHITELISTED_PATH": ['admin/'] 79 | } 80 | 81 | **Note**: Media url, Static url and request-viewer url are automatically excluded. 82 |
83 | 84 | ### Start your server and head to http://localhost:8000/logs/request-viewer to view requests 85 | 86 | ### Head to http://localhost:8000/logs/request-viewer/exceptions to view exceptions 87 | 88 | 89 | View your request logs.
90 | 91 | Screenshot 2021-03-29 at 09 30 33 92 | 93 | ### Contribute 94 | 95 | Well, no big drama, fork the repo and make pull requests, easy-peasy, right? 96 | 97 | ### TODO 98 | * JSON export 99 | * Caching 100 | * Create an African unicorn 101 | * Buy a yacht 102 | 103 | ### Credits 104 | 105 | * Toni Akinmolayan - [twitter](https://twitter.com/toniastro_) [GitHub](https://github.com/toniastro) 106 | * Michael Trojan Okoh - [twitter](https://twitter.com/ichtrojan) [GitHub](https://github.com/ichtrojan) 107 | 108 | 109 | 110 | ### Follow me (I am not boring, I promise) 111 | * [Twitter](https://twitter.com/sirrobot01) 112 | * [Github](https://github.com/sirrobot01) 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /request_viewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirrobot01/django-request-viewer/41e24f97bb4d92212724c8dbf9b275e68c81c39f/request_viewer/__init__.py -------------------------------------------------------------------------------- /request_viewer/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from . import models 5 | 6 | admin.site.register(models.Logger) 7 | -------------------------------------------------------------------------------- /request_viewer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RequestViewerConfig(AppConfig): 5 | name = 'request_viewer' 6 | -------------------------------------------------------------------------------- /request_viewer/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | # Request viewer configs 4 | REQUEST_VIEWER = getattr(settings, 'REQUEST_VIEWER', {}) 5 | LIVE_MONITORING = REQUEST_VIEWER.get('LIVE_MONITORING', True) 6 | WHITELISTED_PATHS = REQUEST_VIEWER.get('WHITELISTED_PATH', []) 7 | # LOG_EXCEPTIONS = REQUEST_VIEWER.get('LOG_EXCEPTIONS') @TODO Exception logger 8 | DATETIME_FORMAT = "%d %b %Y %H:%M:%S.%f" 9 | 10 | # Static and media files 11 | STATIC_URL = getattr(settings, 'STATIC_URL', '/static/') 12 | MEDIA_URL = getattr(settings, 'MEDIA_URL', '/media/') 13 | -------------------------------------------------------------------------------- /request_viewer/middleware.py: -------------------------------------------------------------------------------- 1 | from django.template.response import TemplateResponse 2 | from django.urls import reverse 3 | from django.conf import settings 4 | import os, traceback 5 | 6 | from .models import RequestModel, ResponseModel, Logger, TemplateResponseModel, ExceptionModel 7 | from .conf import WHITELISTED_PATHS, DATETIME_FORMAT, LIVE_MONITORING 8 | from .utils import is_whitelisted, get_time_length 9 | 10 | default_whitelist = [ 11 | os.path.normpath(path) for path in [reverse('request-viewer'), 12 | reverse('modal-content'), settings.MEDIA_URL, settings.STATIC_URL]] 13 | 14 | WHITELISTED_PATHS.extend(default_whitelist) 15 | 16 | 17 | class RequestViewerMiddleware: 18 | 19 | def __init__(self, get_response): 20 | self.get_response = get_response 21 | self.request_obj = {} 22 | self.response_obj = {} 23 | 24 | def __call__(self, request): 25 | path = request.path 26 | if not is_whitelisted(os.path.normpath(path), WHITELISTED_PATHS) and LIVE_MONITORING: 27 | self.request_obj = RequestModel(request).__dict__ 28 | obj, _ = Logger.objects.get_or_create(path=path) 29 | response = self.get_response(request) 30 | if isinstance(response, TemplateResponse): 31 | self.response_obj = TemplateResponseModel(response).__dict__ 32 | else: 33 | self.response_obj = ResponseModel(response).__dict__ 34 | old_data = obj.data 35 | time_length = get_time_length(self.request_obj.get('request_timestamp'), self.response_obj.get('response_timestamp'), DATETIME_FORMAT) 36 | self.request_obj['time_length'] = time_length 37 | parsed_data = dict(self.request_obj, **self.response_obj) 38 | if parsed_data not in old_data: 39 | old_data.append(parsed_data) 40 | 41 | obj.data = old_data 42 | obj.save() 43 | return response 44 | 45 | return self.get_response(request) 46 | 47 | class ExceptionMiddleware: 48 | def __init__(self, get_response): 49 | self.get_response = get_response 50 | 51 | def __call__(self, request): 52 | return self.get_response(request) 53 | 54 | def process_exception(self, request, exception): 55 | path = request.path 56 | if not is_whitelisted(os.path.normpath(path), WHITELISTED_PATHS) and LIVE_MONITORING: 57 | self.run_exception(exception) 58 | 59 | @staticmethod 60 | def run_exception(exception): 61 | tb = exception.__traceback__ 62 | 63 | traces = traceback.extract_tb(tb) 64 | 65 | exec_type = type(exception).__name__ 66 | stacks = [] 67 | for frame in traces: 68 | stack = { 69 | 'filename': frame.filename, 70 | 'lineno': frame.lineno, 71 | 'name': frame.name, 72 | 'line': frame.line, 73 | 'locals': frame.locals 74 | } 75 | stacks.append(stack) 76 | if hasattr(exception, "message"): 77 | message = exception.message 78 | else: 79 | message = None 80 | 81 | last_stack = stacks[-1] if stacks else {} 82 | obj = ExceptionModel( 83 | func_name=last_stack.get("name"), 84 | line_no=last_stack.get("lineno"), 85 | line=last_stack.get("line"), 86 | exc_type=exec_type, 87 | message=message, 88 | stacks=stacks 89 | ) 90 | obj.save() 91 | -------------------------------------------------------------------------------- /request_viewer/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-25 19:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Logger', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('path', models.CharField(blank=True, max_length=255, null=True)), 19 | ('data', models.JSONField(default=list)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /request_viewer/migrations/0002_exceptionmodel_alter_logger_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.1 on 2022-01-17 10:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('request_viewer', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='ExceptionModel', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('exc_type', models.CharField(max_length=255)), 18 | ('message', models.TextField(blank=True, null=True)), 19 | ('func_name', models.CharField(max_length=1024)), 20 | ('line_no', models.IntegerField()), 21 | ('line', models.TextField(blank=True, null=True)), 22 | ('stacks', models.JSONField(default=list)), 23 | ('logged_at', models.DateTimeField(auto_now_add=True)), 24 | ], 25 | ), 26 | migrations.AlterField( 27 | model_name='logger', 28 | name='id', 29 | field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /request_viewer/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirrobot01/django-request-viewer/41e24f97bb4d92212724c8dbf9b275e68c81c39f/request_viewer/migrations/__init__.py -------------------------------------------------------------------------------- /request_viewer/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | import json 3 | from django.conf import settings 4 | from django.http import HttpRequest, StreamingHttpResponse 5 | from datetime import datetime 6 | 7 | from .conf import DATETIME_FORMAT 8 | try: 9 | from django.db.models import JSONField 10 | except ModuleNotFoundError: 11 | try: 12 | from jsonfield import JSONField 13 | except: 14 | from django.contrib.postgres.fields import JSONField 15 | 16 | 17 | # Create your models here. 18 | 19 | 20 | class Logger(models.Model): 21 | path = models.CharField(max_length=255, blank=True, null=True) 22 | data = JSONField(default=list) 23 | 24 | @classmethod 25 | def get_data(cls, filter_by=None, value=None): 26 | data = [] 27 | logs = cls.objects.all() 28 | for log in logs: 29 | log_data = log.data 30 | data.extend(log_data) 31 | return data 32 | 33 | 34 | class BaseClass: 35 | 36 | def to_json(self): 37 | return json.dumps(self.__dict__) 38 | 39 | 40 | class RequestModel(BaseClass): 41 | LOG_EXCEPTIONS = getattr(settings, "LOG_EXCEPTIONS", False) 42 | 43 | def __init__(self, request: HttpRequest): 44 | self.path = request.path 45 | self.method = request.method 46 | self.request_timestamp = datetime.now().strftime(DATETIME_FORMAT) 47 | self.params = dict(request.GET) if self.method == "GET" else dict(request.POST) 48 | self.files = dict(request.FILES) 49 | self.headers = dict(request.headers) 50 | self.authenticated_request = request.user.is_authenticated 51 | 52 | def get_object(self): 53 | return self.__class__(**self.__dict__) 54 | 55 | @property 56 | def __dict__(self): 57 | return { 58 | "path": self.path, 59 | "method": self.method, 60 | "params": self.params, 61 | "files": self.files, 62 | "headers": self.headers, 63 | "request_timestamp": self.request_timestamp 64 | } 65 | 66 | 67 | class BaseResponse(BaseClass): 68 | 69 | def __init__(self, response): 70 | self.status_code = response.status_code 71 | self.status_message = response.reason_phrase 72 | self.content_type = response.charset 73 | self.response_datetime = datetime.now().strftime(DATETIME_FORMAT) 74 | super(BaseResponse, self).__init__() 75 | 76 | @property 77 | def __dict__(self): 78 | return { 79 | "status_code": self.status_code, 80 | "status_message": self.status_message, 81 | "content_type": self.content_type, 82 | "response_timestamp": self.response_datetime 83 | } 84 | 85 | 86 | class ResponseModel(BaseResponse): 87 | 88 | def __init__(self, response): 89 | if isinstance(response, StreamingHttpResponse): 90 | self.message = "StreamingHttpResponse" 91 | elif isinstance(response.content, bytes): 92 | self.message = response.content.decode() 93 | else: 94 | self.message = response.content 95 | super(ResponseModel, self).__init__(response) 96 | 97 | @property 98 | def __dict__(self): 99 | return dict(super(ResponseModel, self).__dict__, **{"message": self.message}) 100 | 101 | class TemplateResponseModel(BaseResponse): 102 | 103 | def __init__(self, response): 104 | self.template_name = response.template_name 105 | context_data = response.context_data 106 | self.view = str(context_data.get('view')) 107 | super(TemplateResponseModel, self).__init__(response) 108 | 109 | @property 110 | def __dict__(self): 111 | data = { 112 | "template_name": self.template_name, 113 | "view": self.view 114 | } 115 | return dict(super(TemplateResponseModel, self).__dict__, **data) 116 | 117 | 118 | class ExceptionModel(models.Model): 119 | exc_type = models.CharField(max_length=255) 120 | message = models.TextField(blank=True, null=True) 121 | func_name = models.CharField(max_length=1024) 122 | line_no = models.IntegerField() 123 | line = models.TextField(blank=True, null=True) 124 | stacks = JSONField(default=list) 125 | logged_at = models.DateTimeField(auto_now_add=True) 126 | 127 | -------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/exception.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exception Logger | Dashboard 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 14 | 17 | 20 | 27 | 28 | 29 | 30 |
31 | Repository 32 |
33 |
34 |

Exception Viewer & Logger [Go to Request Dashboard]

35 |
36 | {% include 'request_viewer/fragments/filter.html' %} 37 | 38 |
39 |
40 | Live Monitoring Status : 41 | {% if is_connected %} 42 | 44 | {% else %} 45 | 47 | {% endif %} 48 |

49 |
50 |
51 |
52 | {% include 'request_viewer/fragments/exception/table.html' with exception=exception %} 53 |
54 |
55 | {% include 'request_viewer/fragments/pagination.html' with paginator=paginator exception=exception %} 56 |
57 |
58 | {% include 'request_viewer/fragments/modal.html' with pageTitle="Exception" %} 59 |
60 | 61 | 62 | 120 | -------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/exception/modal.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/exception/modal_content.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Exception: {{ obj.exc_type }}
Line No: {{ obj.line_no }}
Message: {{ obj.message }}
Logged time: {{ obj.logged_at }}
18 | 29 |
30 |
31 | {{ obj.message }} 32 |
33 |
34 | ----------------------------------------
35 | {% for stack in obj.stacks %} 36 | {% for key, value in stack.items %} 37 | {% if value %} 38 | {{ key }}: {{ value }} 40 | {% endif %} 41 | {% endfor %} 42 |
---------------------------------------- 43 | {% endfor %} 44 |
45 |
46 |
-------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/exception/table.html: -------------------------------------------------------------------------------- 1 | {% load request_view_tag %} 2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 29 | 32 | 33 | 34 | 35 | {% for exception in object_list %} 36 | 37 | 40 | 43 | 46 | 49 | 52 | 56 | 57 | {% endfor %} 58 | 59 |
11 | Type 12 | 15 | Function 16 | 19 | Line Number 20 | 23 | Message 24 | 27 | Timestamp 28 | 30 | 31 |
38 | {{ exception.exc_type }} 39 | 41 | {{ exception.func_name }} 42 | 44 | {{ exception.line_no }} 45 | 47 | {{ exception.message }} 48 | 50 | {{ exception.logged_at }} 51 | 53 | Details 55 |
60 |
61 |
62 |
63 | -------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/filter.html: -------------------------------------------------------------------------------- 1 |
2 | {% if entity == "request" %} 3 |
4 |
5 | 13 |
15 | 16 | 17 | 18 |
19 |
20 |
21 | {% endif %} 22 |
23 | 24 | 25 | 27 | 28 | 29 | 30 | 32 |
33 |
-------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/modal.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/pagination.html: -------------------------------------------------------------------------------- 1 |
2 | 44 |
-------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/request/filter.html: -------------------------------------------------------------------------------- 1 |
2 | {% if entity == "request" %} 3 |
4 |
5 | 13 |
15 | 16 | 17 | 18 |
19 |
20 |
21 | {% endif %} 22 | 23 |
24 | 25 | 26 | 28 | 29 | 30 | 31 | 33 |
34 |
-------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/request/modal_content.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% if obj.template_name %} 20 | 21 | 22 | 23 | {% endif %} 24 | 25 |
Host: {{ obj.headers.Host }}
Method: {{ obj.method }}
Status: {{ obj.status_code }}
Path: {{ obj.path }}
Response content type: {{ obj.content_type }}
Templates: {{ obj.template_name }}
26 | 45 |
46 |
47 | { 48 | {% for key, value in obj.headers.items %} 49 | {% if value %} 50 | {{ key }}: 51 | {{ value }} 52 | {% endif %} 53 | {% endfor %} 54 | } 55 |
56 |
57 | { 58 | {% for key, value in obj.params.items %} 59 | {% if value %} 60 | {{ key }}: {{ value }} 62 | {% endif %} 63 | {% endfor %} 64 | } 65 |
66 |
67 | {% if obj.message %} 68 | {% if obj.content_type == "application/json" %} 69 | { 70 | {% for key, value in obj.messsage.items %} 71 | {{ key }}: {{ value }} 73 | {% endfor %} 74 | } 75 | {% else %} 76 | {{ obj.message|safe }} 77 | {% endif %} 78 | {% else %} 79 | { } 80 | {% endif %} 81 |
82 |
83 | { 84 | {% for key, value in obj.files.items %} 85 | {% if value %} 86 | {{ key }}: {{ value }} 88 | {% endif %} 89 | {% endfor %} 90 | } 91 |
92 |
93 |
-------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/fragments/request/table.html: -------------------------------------------------------------------------------- 1 | {% load request_view_tag %} 2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 | 13 | 17 | 21 | 25 | 29 | 33 | 36 | 37 | 38 | 39 | {% for path in object_list %} 40 | 41 | 44 | 47 | 53 | 56 | 60 | 63 | 67 | 68 | {% endfor %} 69 | 70 |
11 | Method 12 | 15 | Path 16 | 19 | Status code 20 | 23 | Status message 24 | 27 | Duration 28 | 31 | Timestamp 32 | 34 | 35 |
42 | {{ path.method }} 43 | 45 | {{ path.path }} 46 | 48 | 50 | {{ path.status_code }} 51 | 52 | 54 | {{ path.status_message }} 55 | 57 | {% if path.time_length %} {{ path.time_length }}s {% endif %} 58 | 59 | 61 | {{ path.request_timestamp }} 62 | 64 | Details 66 |
71 |
72 |
73 |
74 | -------------------------------------------------------------------------------- /request_viewer/templates/request_viewer/request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Request Logger | Dashboard 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 14 | 17 | 20 | 27 | 28 | 29 | 30 |
31 | Repository 32 |
33 |
34 |

Request Viewer & Logger [Go to Exception Dashboard]

35 |
36 | {% include 'request_viewer/fragments/filter.html' with entity="request" %} 37 | 38 |
39 |
40 | Live Monitoring Status : {% if is_connected %} 41 | 43 | {% else %} 44 | 46 | {% endif %} 47 |

48 |
49 |
50 |
51 | {% include 'request_viewer/fragments/request/table.html' with paths=paths %} 52 |
53 |
54 | {% include 'request_viewer/fragments/pagination.html' with paginator=paginator queryset=paths %} 55 |
56 |
57 | {% include 'request_viewer/fragments/modal.html' with pageTitle="Request" %} 58 |
59 | 60 | 61 | 122 | -------------------------------------------------------------------------------- /request_viewer/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sirrobot01/django-request-viewer/41e24f97bb4d92212724c8dbf9b275e68c81c39f/request_viewer/templatetags/__init__.py -------------------------------------------------------------------------------- /request_viewer/templatetags/request_view_tag.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | import json 3 | 4 | register = template.Library() 5 | 6 | @register.filter('startswith') 7 | def startswith(text, starts): 8 | texts = str(starts).split(',') 9 | return any([str(text).startswith(tx) for tx in texts]) 10 | 11 | 12 | @register.filter('to_json') 13 | def to_json(ls_string): 14 | return json.dumps(ls_string) 15 | 16 | -------------------------------------------------------------------------------- /request_viewer/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /request_viewer/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import RequestDashboard, ExceptionDashboard, get_modal_content 3 | 4 | urlpatterns = [ 5 | path('request-viewer/', RequestDashboard.as_view(), name="request-viewer"), 6 | path('request-viewer/exceptions', ExceptionDashboard.as_view(), name="exception-viewer"), 7 | path('modal-content/', get_modal_content, name='modal-content') 8 | ] 9 | -------------------------------------------------------------------------------- /request_viewer/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | 5 | def is_whitelisted(norm_path, whitelisted_paths=None): 6 | is_whitelist = False 7 | 8 | if norm_path.endswith('.ico'): 9 | return True 10 | 11 | if whitelisted_paths is None: 12 | whitelisted_paths = [] 13 | 14 | for path in whitelisted_paths: 15 | path = '/' + path if not path.startswith('/') else path 16 | path = path[:-1] if path.endswith('/') else path 17 | if os.path.normpath(norm_path).startswith(path): 18 | is_whitelist = True 19 | break 20 | 21 | return is_whitelist 22 | 23 | 24 | def get_time_length(request_timestamp, response_timestamp, datetime_format): 25 | diff = datetime.strptime(response_timestamp, datetime_format) - datetime.strptime(request_timestamp, 26 | datetime_format) 27 | return diff.total_seconds() 28 | 29 | 30 | # View utils 31 | 32 | def is_admin(user): 33 | return user.is_superuser 34 | 35 | 36 | def filter_paths(paths, filter_by, value): 37 | if filter_by == "method": 38 | if value: 39 | return [path for path in paths if path.get('method') == value] 40 | else: 41 | return paths 42 | else: 43 | if value: 44 | return [path for path in paths if any([value.lower() in [str(val).lower() for val in path.values()]])] 45 | else: 46 | return paths 47 | -------------------------------------------------------------------------------- /request_viewer/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.decorators import user_passes_test 3 | from django.core import serializers 4 | from django.shortcuts import render 5 | from django.views import generic 6 | from django.views.decorators.csrf import csrf_exempt 7 | from django.core.paginator import Paginator 8 | import json 9 | # Create your views here. 10 | from request_viewer.models import Logger, ExceptionModel 11 | 12 | from django.utils.decorators import method_decorator 13 | from .utils import is_admin, filter_paths 14 | from .conf import LIVE_MONITORING 15 | 16 | 17 | @method_decorator(csrf_exempt, name='dispatch') 18 | @method_decorator(user_passes_test(is_admin), name='dispatch') 19 | class BaseView(generic.ListView): 20 | paginator = None 21 | page = 1 22 | middleware = None 23 | object_list = None 24 | 25 | def serialize_queryset(self): 26 | raise NotImplementedError('`serialize_queryset()` must be implemented.') 27 | 28 | def middleware_used(self): 29 | return self.middleware and self.middleware in settings.MIDDLEWARE 30 | 31 | def get_extra_data(self): 32 | self.page = self.request.GET.get('page', self.page) 33 | self.paginator = Paginator(self.serialize_queryset(), 10) 34 | 35 | def get_context_data(self, *args, **kwargs): 36 | context = super(BaseView, self).get_context_data(*args, **kwargs) 37 | self.get_extra_data() 38 | context['paginator'] = self.paginator 39 | context["is_connected"] = LIVE_MONITORING and self.middleware_used() 40 | context['object_list'] = self.paginator.page(self.page) 41 | return context 42 | 43 | 44 | class RequestDashboard(BaseView): 45 | template_name = "request_viewer/request.html" 46 | model = Logger 47 | paginator = None 48 | middleware = "request_viewer.middleware.RequestViewerMiddleware" 49 | 50 | def serialize_queryset(self): 51 | return self.model.get_data() 52 | 53 | def post(self, request, *args, **kwargs): 54 | self.template_name = "request_viewer/fragments/request/table.html" 55 | filter_by = request.POST.get('filterBy') 56 | value = request.POST.get('value') 57 | page = request.POST.get('page', 1) 58 | page = 1 if not page else page 59 | self.get_extra_data() 60 | paths = filter_paths(self.paginator.page(page), filter_by, value) 61 | context = super(RequestDashboard, self).get_context_data(*args, **kwargs) 62 | context["object_list"] = paths 63 | return render(request, self.template_name, context) 64 | 65 | 66 | class ExceptionDashboard(BaseView): 67 | template_name = "request_viewer/exception.html" 68 | paginator = None 69 | queryset = ExceptionModel.objects.all() 70 | middleware = "request_viewer.middleware.ExceptionMiddleware" 71 | 72 | def serialize_queryset(self): 73 | obj = json.loads(serializers.serialize("json", self.queryset)) 74 | q = [x.get("fields") for x in obj] 75 | return q 76 | 77 | def post(self, request, **kwargs): 78 | self.template_name = "request_viewer/fragments/exception/table.html" 79 | filter_by = request.POST.get('filterBy') 80 | value = request.POST.get('value') 81 | page = request.POST.get('page', 1) 82 | page = 1 if not page else page 83 | self.get_extra_data() 84 | exceptions = self.paginator.page(page) 85 | context = {'exceptions': exceptions, 'is_connected': LIVE_MONITORING} 86 | return render(request, self.template_name, context) 87 | 88 | 89 | @csrf_exempt 90 | def get_modal_content(request): 91 | obj = json.loads(request.POST.get('obj')) 92 | entity = request.POST.get('entity', "request") 93 | return render(request, f'request_viewer/fragments/{entity}/modal_content.html', {'obj': obj}) 94 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools==65.6.3 2 | django-jsonfield~=1.4.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | def read(f): 5 | return open(f, 'r', encoding='utf-8').read() 6 | 7 | 8 | setup( 9 | name="django-request-viewer", 10 | version='1.0.1', 11 | description="Log and view requests and exceptions made on your Django app", 12 | long_description=read("README.md"), 13 | long_description_content_type='text/markdown', 14 | url="https://github.com/sirrobot01/django-request-viewer", 15 | author="Mukhtar Akere", 16 | author_email="akeremukhtar10@gmail.com", 17 | license="BSD-3-Clause", 18 | classifiers=[ 19 | "Environment :: Web Environment", 20 | "Framework :: Django", 21 | "Framework :: Django :: 3.1", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3 :: Only", 28 | "Programming Language :: Python :: 3.6", 29 | "Programming Language :: Python :: 3.7", 30 | "Programming Language :: Python :: 3.8", 31 | "Topic :: Internet :: WWW/HTTP", 32 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 33 | ], 34 | install_requires=[ 35 | "django>=2.2" 36 | ], 37 | packages=find_packages(), 38 | include_package_data=True, 39 | extras_require={}, 40 | project_urls={ 41 | 'Source': 'https://github.com/sirrobot01/django-request-viewer', 42 | }, 43 | ) 44 | --------------------------------------------------------------------------------