Skip to content

Commit 91b5df5

Browse files
Merge pull request #105 from vishalanandl177/master
Master
2 parents 51cb174 + f0f9fd7 commit 91b5df5

File tree

11 files changed

+438
-117
lines changed

11 files changed

+438
-117
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# DRF API Logger
2-
![version](https://img.shields.io/badge/version-1.1.16-blue.svg)
2+
![version](https://img.shields.io/badge/version-1.1.19-blue.svg)
33
[![Downloads](https://static.pepy.tech/personalized-badge/drf-api-logger?period=total&units=none&left_color=black&right_color=orange&left_text=Downloads%20Total)](http://pepy.tech/project/drf-api-logger)
44
[![Downloads](https://static.pepy.tech/personalized-badge/drf-api-logger?period=month&units=none&left_color=black&right_color=orange&left_text=Downloads%20Last%20Month)](https://pepy.tech/project/drf-api-logger)
55
[![Open Source](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://opensource.org/)

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ DRF API Logger Model:
414414
older data. To improve the searching or filtering, try to add indexes in
415415
the drf_api_logs table.
416416

417-
.. |version| image:: https://img.shields.io/badge/version-1.1.16-blue.svg
417+
.. |version| image:: https://img.shields.io/badge/version-1.1.19-blue.svg
418418
.. |Downloads| image:: https://static.pepy.tech/personalized-badge/drf-api-logger?period=total&units=none&left_color=black&right_color=orange&left_text=Downloads%20Total
419419
:target: http://pepy.tech/project/drf-api-logger
420420
.. |image1| image:: https://static.pepy.tech/personalized-badge/drf-api-logger?period=month&units=none&left_color=black&right_color=orange&left_text=Downloads%20Last%20Month

drf_api_logger/admin.py

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,118 @@
11
from datetime import timedelta
2-
32
from django.conf import settings
43
from django.contrib import admin
54
from django.db.models import Count
65
from django.http import HttpResponse
7-
86
from drf_api_logger.utils import database_log_enabled
97

8+
# Ensure the API log model and related features are only used if enabled in settings
109
if database_log_enabled():
1110
from drf_api_logger.models import APILogsModel
1211
from django.utils.translation import gettext_lazy as _
1312
import csv
1413

15-
1614
class ExportCsvMixin:
15+
"""
16+
Mixin class to enable exporting selected queryset to CSV in Django admin.
17+
"""
18+
1719
def export_as_csv(self, request, queryset):
20+
"""
21+
Export selected objects as a CSV file.
22+
"""
1823
meta = self.model._meta
1924
field_names = [field.name for field in meta.fields]
2025

2126
response = HttpResponse(content_type='text/csv')
22-
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
27+
response['Content-Disposition'] = f'attachment; filename={meta}.csv'
2328
writer = csv.writer(response)
2429

30+
# Write headers
2531
writer.writerow(field_names)
32+
# Write rows
2633
for obj in queryset:
2734
writer.writerow([getattr(obj, field) for field in field_names])
2835

2936
return response
3037

3138
export_as_csv.short_description = "Export Selected"
3239

40+
3341
class SlowAPIsFilter(admin.SimpleListFilter):
34-
title = _('API Performance')
42+
"""
43+
Custom filter for Django admin to categorize API logs as 'slow' or 'fast'
44+
based on execution time threshold.
45+
"""
3546

36-
# Parameter for the filter that will be used in the URL query.
47+
title = _('API Performance')
3748
parameter_name = 'api_performance'
3849

3950
def __init__(self, request, params, model, model_admin):
4051
super().__init__(request, params, model, model_admin)
52+
# Load and convert the slow API threshold setting from milliseconds to seconds
4153
if hasattr(settings, 'DRF_API_LOGGER_SLOW_API_ABOVE'):
42-
if isinstance(settings.DRF_API_LOGGER_SLOW_API_ABOVE, int): # Making sure for integer value.
43-
self._DRF_API_LOGGER_SLOW_API_ABOVE = settings.DRF_API_LOGGER_SLOW_API_ABOVE / 1000 # Converting to seconds.
54+
if isinstance(settings.DRF_API_LOGGER_SLOW_API_ABOVE, int):
55+
self._DRF_API_LOGGER_SLOW_API_ABOVE = settings.DRF_API_LOGGER_SLOW_API_ABOVE / 1000
4456

4557
def lookups(self, request, model_admin):
4658
"""
47-
Returns a list of tuples. The first element in each
48-
tuple is the coded value for the option that will
49-
appear in the URL query. The second element is the
50-
human-readable name for the option that will appear
51-
in the right sidebar.
59+
Returns lookup options for the filter in the admin sidebar.
5260
"""
5361
slow = 'Slow'
5462
fast = 'Fast'
5563
if hasattr(settings, 'DRF_API_LOGGER_SLOW_API_ABOVE'):
56-
slow += ', >={}ms'.format(settings.DRF_API_LOGGER_SLOW_API_ABOVE)
57-
fast += ', <{}ms'.format(settings.DRF_API_LOGGER_SLOW_API_ABOVE)
58-
64+
slow += f', >={settings.DRF_API_LOGGER_SLOW_API_ABOVE}ms'
65+
fast += f', <{settings.DRF_API_LOGGER_SLOW_API_ABOVE}ms'
5966
return (
6067
('slow', _(slow)),
6168
('fast', _(fast)),
6269
)
6370

6471
def queryset(self, request, queryset):
6572
"""
66-
Returns the filtered queryset based on the value
67-
provided in the query string and retrievable via
68-
`self.value()`.
73+
Returns filtered queryset depending on whether 'slow' or 'fast'
74+
option is selected in the filter.
6975
"""
70-
# to decide how to filter the queryset.
7176
if self.value() == 'slow':
7277
return queryset.filter(execution_time__gte=self._DRF_API_LOGGER_SLOW_API_ABOVE)
7378
if self.value() == 'fast':
7479
return queryset.filter(execution_time__lt=self._DRF_API_LOGGER_SLOW_API_ABOVE)
75-
7680
return queryset
7781

82+
7883
class APILogsAdmin(admin.ModelAdmin, ExportCsvMixin):
84+
"""
85+
Custom admin class for the API logs model with filters, charts, export functionality,
86+
and restricted permissions.
87+
"""
7988

8089
actions = ["export_as_csv"]
8190

8291
def __init__(self, model, admin_site):
8392
super().__init__(model, admin_site)
93+
8494
self._DRF_API_LOGGER_TIMEDELTA = 0
95+
96+
# Conditionally add the slow API filter if setting is provided
8597
if hasattr(settings, 'DRF_API_LOGGER_SLOW_API_ABOVE'):
86-
if isinstance(settings.DRF_API_LOGGER_SLOW_API_ABOVE, int): # Making sure for integer value.
98+
if isinstance(settings.DRF_API_LOGGER_SLOW_API_ABOVE, int):
8799
self.list_filter += (SlowAPIsFilter,)
100+
101+
# Time delta used for adjusting timestamp display
88102
if hasattr(settings, 'DRF_API_LOGGER_TIMEDELTA'):
89-
if isinstance(settings.DRF_API_LOGGER_TIMEDELTA, int): # Making sure for integer value.
103+
if isinstance(settings.DRF_API_LOGGER_TIMEDELTA, int):
90104
self._DRF_API_LOGGER_TIMEDELTA = settings.DRF_API_LOGGER_TIMEDELTA
91105

92106
def added_on_time(self, obj):
107+
"""
108+
Returns formatted 'added_on' timestamp adjusted by timedelta setting.
109+
"""
93110
return (obj.added_on + timedelta(minutes=self._DRF_API_LOGGER_TIMEDELTA)).strftime("%d %b %Y %H:%M:%S")
94111

95112
added_on_time.admin_order_field = 'added_on'
96113
added_on_time.short_description = 'Added on'
97114

115+
# Admin UI settings
98116
list_per_page = 20
99117
list_display = ('id', 'api', 'method', 'status_code', 'execution_time', 'added_on_time',)
100118
list_filter = ('added_on', 'status_code', 'method',)
@@ -105,52 +123,77 @@ def added_on_time(self, obj):
105123
)
106124
exclude = ('added_on',)
107125

126+
# Custom admin templates
108127
change_list_template = 'charts_change_list.html'
109128
change_form_template = 'change_form.html'
110129
date_hierarchy = 'added_on'
111130

112131
def changelist_view(self, request, extra_context=None):
132+
"""
133+
Override to inject custom chart data for status codes and analytics into the context.
134+
"""
113135
response = super(APILogsAdmin, self).changelist_view(request, extra_context)
114136
try:
115137
filtered_query_set = response.context_data["cl"].queryset
116138
except Exception:
117139
return response
118-
analytics_model = filtered_query_set.values('added_on__date').annotate(total=Count('id')).order_by('total')
140+
141+
# Aggregate logs by date
142+
analytics_model = filtered_query_set.values('added_on__date').annotate(
143+
total=Count('id')
144+
).order_by('total')
145+
146+
# Count each unique status code
119147
status_code_count_mode = filtered_query_set.values('id').values('status_code').annotate(
120148
total=Count('id')).order_by('status_code')
121-
status_code_count_keys = list()
122-
status_code_count_values = list()
123-
for item in status_code_count_mode:
124-
status_code_count_keys.append(item.get('status_code'))
125-
status_code_count_values.append(item.get('total'))
149+
150+
status_code_count_keys = [item.get('status_code') for item in status_code_count_mode]
151+
status_code_count_values = [item.get('total') for item in status_code_count_mode]
152+
153+
# Add chart data to context
126154
extra_context = dict(
127155
analytics=analytics_model,
128156
status_code_count_keys=status_code_count_keys,
129157
status_code_count_values=status_code_count_values
130158
)
159+
131160
response.context_data.update(extra_context)
132161
return response
133162

134163
def get_queryset(self, request):
164+
"""
165+
Ensure the queryset uses the correct database as configured in settings.
166+
"""
135167
drf_api_logger_default_database = 'default'
136168
if hasattr(settings, 'DRF_API_LOGGER_DEFAULT_DATABASE'):
137169
drf_api_logger_default_database = settings.DRF_API_LOGGER_DEFAULT_DATABASE
138170
return super(APILogsAdmin, self).get_queryset(request).using(drf_api_logger_default_database)
139171

140172
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
173+
"""
174+
If `export` is in the query parameters, return CSV export of a single object.
175+
"""
141176
if request.GET.get('export', False):
142177
drf_api_logger_default_database = 'default'
143178
if hasattr(settings, 'DRF_API_LOGGER_DEFAULT_DATABASE'):
144179
drf_api_logger_default_database = settings.DRF_API_LOGGER_DEFAULT_DATABASE
180+
145181
export_queryset = self.get_queryset(request).filter(pk=object_id).using(drf_api_logger_default_database)
146182
return self.export_as_csv(request, export_queryset)
183+
147184
return super(APILogsAdmin, self).changeform_view(request, object_id, form_url, extra_context)
148185

149186
def has_add_permission(self, request, obj=None):
187+
"""
188+
Prevent adding logs from the admin.
189+
"""
150190
return False
151191

152192
def has_change_permission(self, request, obj=None):
193+
"""
194+
Prevent modifying logs from the admin.
195+
"""
153196
return False
154197

155-
198+
# Register the model with the custom admin class
156199
admin.site.register(APILogsModel, APILogsAdmin)

drf_api_logger/apps.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,50 @@
1+
import os
2+
13
from django.apps import AppConfig
24

5+
from drf_api_logger.utils import database_log_enabled
6+
7+
# Global variable to hold the reference to the logger thread
8+
LOGGER_THREAD = None
9+
310

411
class LoggerConfig(AppConfig):
12+
"""
13+
Django AppConfig for the DRF API Logger app.
14+
This is the configuration class for the Django app named 'drf_api_logger'.
15+
It is used to initialize application-level settings and perform startup tasks.
16+
"""
517
name = 'drf_api_logger'
618
verbose_name = 'DRF API Logger'
19+
20+
def ready(self):
21+
"""
22+
Called when the app is ready.
23+
Starts the background thread that inserts API logs into the database,
24+
but only in the main process (to avoid duplication on autoreload).
25+
"""
26+
global LOGGER_THREAD
27+
28+
# Prevent running this logic in the autoreloader subprocess (used during development)
29+
if os.environ.get('RUN_MAIN') == 'true':
30+
# Check if database logging is enabled via settings
31+
if database_log_enabled():
32+
from drf_api_logger.insert_log_into_database import InsertLogIntoDatabase
33+
import threading
34+
35+
LOG_THREAD_NAME = 'insert_log_into_database'
36+
37+
# Check if the thread is already running to avoid starting multiple threads
38+
already_exists = False
39+
for t in threading.enumerate():
40+
if t.name == LOG_THREAD_NAME:
41+
already_exists = True
42+
break
43+
44+
# If the thread is not already running, create and start it
45+
if not already_exists:
46+
t = InsertLogIntoDatabase() # This is a subclass of threading.Thread
47+
t.daemon = True # Make it a daemon so it shuts down with the main thread
48+
t.name = LOG_THREAD_NAME # Assign a name to the thread for easy identification
49+
t.start() # Start the background log insertion thread
50+
LOGGER_THREAD = t

0 commit comments

Comments
 (0)