├── .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 |
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 |
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 |
33 |
34 |
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 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/request_viewer/templates/request_viewer/fragments/exception/modal_content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Exception: {{ obj.exc_type }} |
6 |
7 |
8 | Line No: {{ obj.line_no }} |
9 |
10 |
11 | Message: {{ obj.message }} |
12 |
13 |
14 | Logged time: {{ obj.logged_at }} |
15 |
16 |
17 |
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 |
11 | Type
12 | |
13 |
15 | Function
16 | |
17 |
19 | Line Number
20 | |
21 |
23 | Message
24 | |
25 |
27 | Timestamp
28 | |
29 |
30 |
31 | |
32 |
33 |
34 |
35 | {% for exception in object_list %}
36 |
37 |
38 | {{ exception.exc_type }}
39 | |
40 |
41 | {{ exception.func_name }}
42 | |
43 |
44 | {{ exception.line_no }}
45 | |
46 |
47 | {{ exception.message }}
48 | |
49 |
50 | {{ exception.logged_at }}
51 | |
52 |
53 | Details
55 | |
56 |
57 | {% endfor %}
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/request_viewer/templates/request_viewer/fragments/filter.html:
--------------------------------------------------------------------------------
1 |
2 | {% if entity == "request" %}
3 |
4 |
5 |
13 |
19 |
20 |
21 | {% endif %}
22 |
23 |
24 |
29 |
30 |
32 |
33 |
--------------------------------------------------------------------------------
/request_viewer/templates/request_viewer/fragments/modal.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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 |
19 |
20 |
21 | {% endif %}
22 |
23 |
24 |
25 |
30 |
31 |
33 |
34 |
--------------------------------------------------------------------------------
/request_viewer/templates/request_viewer/fragments/request/modal_content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Host: {{ obj.headers.Host }} |
6 |
7 |
8 | Method: {{ obj.method }} |
9 |
10 |
11 | Status: {{ obj.status_code }} |
12 |
13 |
14 | Path: {{ obj.path }} |
15 |
16 |
17 | Response content type: {{ obj.content_type }} |
18 |
19 | {% if obj.template_name %}
20 |
21 | Templates: {{ obj.template_name }} |
22 |
23 | {% endif %}
24 |
25 |
26 |
45 |
46 |
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 |
11 | Method
12 | |
13 |
15 | Path
16 | |
17 |
19 | Status code
20 | |
21 |
23 | Status message
24 | |
25 |
27 | Duration
28 | |
29 |
31 | Timestamp
32 | |
33 |
34 |
35 | |
36 |
37 |
38 |
39 | {% for path in object_list %}
40 |
41 |
42 | {{ path.method }}
43 | |
44 |
45 | {{ path.path }}
46 | |
47 |
48 |
50 | {{ path.status_code }}
51 |
52 | |
53 |
54 | {{ path.status_message }}
55 | |
56 |
57 | {% if path.time_length %} {{ path.time_length }}s {% endif %}
58 |
59 | |
60 |
61 | {{ path.request_timestamp }}
62 | |
63 |
64 | Details
66 | |
67 |
68 | {% endfor %}
69 |
70 |
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 |
33 |
34 |
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 |
--------------------------------------------------------------------------------