Skip to content

Commit 08cd2be

Browse files
committed
Remove error_type from model — keep in-memory only
error_type is useful for signals, OTel spans, and metrics but doesn't need to be persisted in the DB. Removed the field and migration. - error_type still captured in middleware and flows through: - Signal payload (for custom consumers) - OTel span attributes - Prometheus metrics (per_error_type) - error_type excluded from DB payload via d.pop('error_type') - Admin error analytics now groups by endpoint + status_code - No migration needed — one fewer DB column https://claude.ai/code/session_01Xm34otsnCyKstNXxvzgABT
1 parent 305f846 commit 08cd2be

6 files changed

Lines changed: 24 additions & 106 deletions

File tree

drf_api_logger/admin.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,17 +168,16 @@ def __init__(self, model, admin_site):
168168
if settings.DRF_API_LOGGER_ENABLE_PROFILING:
169169
self._DRF_API_LOGGER_ENABLE_PROFILING = True
170170
self.list_display = (
171-
'id', 'api', 'method', 'status_code', 'error_type',
171+
'id', 'api', 'method', 'status_code',
172172
'execution_time', 'sql_query_count', 'added_on_time',
173173
)
174174
self.list_filter += (HighQueryCountFilter,)
175175
self.readonly_fields = (
176176
'execution_time', 'client_ip_address', 'api',
177177
'headers', 'body', 'method', 'response', 'status_code',
178-
'error_type', 'added_on_time', 'profiling_breakdown',
178+
'added_on_time', 'profiling_breakdown',
179179
)
180180
self.exclude = ('added_on', 'profiling_data', 'sql_query_count')
181-
self.search_fields = ('body', 'response', 'headers', 'api', 'error_type')
182181

183182
def added_on_time(self, obj):
184183
"""
@@ -366,26 +365,26 @@ def changelist_view(self, request, extra_context=None):
366365
extra_context['sql_distribution'] = list(sql_distribution)
367366
extra_context['profiling_enabled'] = True
368367

369-
# Error analytics: group by endpoint + error type
368+
# Error analytics: group by endpoint + status code
370369
error_qs = filtered_query_set.filter(status_code__gte=400)
371370
if error_qs.exists():
372371
errors_by_endpoint = list(
373-
error_qs.values('api', 'status_code', 'error_type')
372+
error_qs.values('api', 'status_code')
374373
.annotate(
375374
count=Count('id'),
376375
last_seen=Max('added_on'),
377376
)
378377
.order_by('-count')[:20]
379378
)
380-
errors_by_type = list(
381-
error_qs.values('error_type')
379+
errors_by_status = list(
380+
error_qs.values('status_code')
382381
.annotate(count=Count('id'))
383382
.order_by('-count')[:10]
384383
)
385384
error_count = error_qs.count()
386385
total_count = filtered_query_set.count()
387386
extra_context['errors_by_endpoint'] = errors_by_endpoint
388-
extra_context['errors_by_type'] = errors_by_type
387+
extra_context['errors_by_status'] = errors_by_status
389388
extra_context['error_count'] = error_count
390389
extra_context['error_rate'] = round(
391390
(error_count / total_count) * 100, 1

drf_api_logger/middleware/api_logger_middleware.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ def __call__(self, request):
320320

321321
if self.DRF_API_LOGGER_DATABASE and LOGGER_THREAD:
322322
d = data.copy()
323+
d.pop('error_type', None)
323324
d['headers'] = json.dumps(d['headers'], indent=4, ensure_ascii=False) if d.get('headers') else ''
324325
if request_data:
325326
d['body'] = json.dumps(d['body'], indent=4, ensure_ascii=False) if d.get('body') else ''

drf_api_logger/migrations/0004_apilogsmodel_error_type.py

Lines changed: 0 additions & 16 deletions
This file was deleted.

drf_api_logger/models.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,6 @@ class APILogsModel(BaseModel):
8787
blank=True,
8888
default=None,
8989
)
90-
error_type = models.CharField(
91-
max_length=256,
92-
null=True,
93-
blank=True,
94-
default=None,
95-
db_index=True,
96-
)
9790

9891
def __str__(self):
9992
"""

drf_api_logger/templates/charts_change_list.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@
171171
<tr style="background:var(--darkened-bg,#f8f9fa);">
172172
<th style="padding:6px 12px;text-align:left;font-size:12px;color:var(--body-quiet-color,#6c757d);">Endpoint</th>
173173
<th style="padding:6px 12px;text-align:center;font-size:12px;color:var(--body-quiet-color,#6c757d);">Status</th>
174-
<th style="padding:6px 12px;text-align:left;font-size:12px;color:var(--body-quiet-color,#6c757d);">Error Type</th>
175174
<th style="padding:6px 12px;text-align:right;font-size:12px;color:var(--body-quiet-color,#6c757d);">Count</th>
176175
</tr>
177176
{% for err in errors_by_endpoint %}
@@ -180,19 +179,20 @@
180179
<td style="padding:6px 12px;text-align:center;">
181180
<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;{% if err.status_code >= 500 %}background:rgba(220,53,69,0.15);color:#dc3545;{% else %}background:rgba(255,193,7,0.15);color:#ffc107;{% endif %}">{{ err.status_code }}</span>
182181
</td>
183-
<td style="padding:6px 12px;font-size:12px;font-family:monospace;color:var(--body-fg,#212529);">{{ err.error_type|default:"-" }}</td>
184182
<td style="padding:6px 12px;text-align:right;font-weight:700;font-size:14px;color:var(--body-fg,#212529);">{{ err.count }}</td>
185183
</tr>
186184
{% endfor %}
187185
</table>
188186
</div>
189187
<div style="flex:1;min-width:250px;border-left:1px solid var(--hairline-color,#dee2e6);">
190-
<div style="padding:10px 16px;font-weight:600;font-size:13px;color:var(--body-quiet-color,#6c757d);border-bottom:1px solid var(--hairline-color,#f0f0f0);">Errors by Type</div>
188+
<div style="padding:10px 16px;font-weight:600;font-size:13px;color:var(--body-quiet-color,#6c757d);border-bottom:1px solid var(--hairline-color,#f0f0f0);">Errors by Status Code</div>
191189
<table style="width:100%;border-collapse:collapse;">
192-
{% for et in errors_by_type %}
190+
{% for es in errors_by_status %}
193191
<tr style="border-bottom:1px solid var(--hairline-color,#f0f0f0);">
194-
<td style="padding:6px 12px;font-size:12px;font-family:monospace;color:var(--body-fg,#212529);">{{ et.error_type|default:"Unknown" }}</td>
195-
<td style="padding:6px 12px;text-align:right;font-weight:700;font-size:14px;color:var(--body-fg,#212529);">{{ et.count }}</td>
192+
<td style="padding:6px 12px;">
193+
<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;{% if es.status_code >= 500 %}background:rgba(220,53,69,0.15);color:#dc3545;{% else %}background:rgba(255,193,7,0.15);color:#ffc107;{% endif %}">{{ es.status_code }}</span>
194+
</td>
195+
<td style="padding:6px 12px;text-align:right;font-weight:700;font-size:14px;color:var(--body-fg,#212529);">{{ es.count }}</td>
196196
</tr>
197197
{% endfor %}
198198
</table>

tests/test_errors.py

Lines changed: 10 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""
22
Test cases for error highlighting, grouping, and frequency tracking.
3+
Error type is captured in-memory (signals, OTel, metrics) but NOT stored in DB.
34
"""
45
import json
56
from unittest.mock import Mock, patch
@@ -86,7 +87,7 @@ def _make_response(self, status, body):
8687
return HttpResponse(
8788
json.dumps(body),
8889
content_type='application/json',
89-
status=status
90+
status=status,
9091
)
9192

9293
@override_settings(DRF_API_LOGGER_SIGNAL=True)
@@ -114,31 +115,6 @@ def listener(**kwargs):
114115
finally:
115116
API_LOGGER_SIGNAL.listen -= listener
116117

117-
@override_settings(DRF_API_LOGGER_SIGNAL=True)
118-
@patch('drf_api_logger.middleware.api_logger_middleware.resolve')
119-
def test_error_type_in_signal_for_500(self, mock_resolve):
120-
mock_resolve.return_value.namespace = None
121-
mock_resolve.return_value.url_name = 'test'
122-
123-
def get_response(request):
124-
return self._make_response(500, {'detail': 'Internal server error'})
125-
126-
signal_data = []
127-
from drf_api_logger import API_LOGGER_SIGNAL
128-
def listener(**kwargs):
129-
signal_data.append(kwargs)
130-
131-
API_LOGGER_SIGNAL.listen += listener
132-
try:
133-
middleware = APILoggerMiddleware(get_response=get_response)
134-
request = self.factory.get('/api/test/')
135-
middleware(request)
136-
137-
self.assertEqual(len(signal_data), 1)
138-
self.assertEqual(signal_data[0]['error_type'], 'Internal server error')
139-
finally:
140-
API_LOGGER_SIGNAL.listen -= listener
141-
142118
@override_settings(DRF_API_LOGGER_SIGNAL=True)
143119
@patch('drf_api_logger.middleware.api_logger_middleware.resolve')
144120
def test_no_error_type_for_2xx(self, mock_resolve):
@@ -164,12 +140,10 @@ def listener(**kwargs):
164140
finally:
165141
API_LOGGER_SIGNAL.listen -= listener
166142

167-
@override_settings(
168-
DRF_API_LOGGER_DATABASE=True,
169-
)
143+
@override_settings(DRF_API_LOGGER_DATABASE=True)
170144
@patch('drf_api_logger.middleware.api_logger_middleware.resolve')
171145
@patch('drf_api_logger.middleware.api_logger_middleware.LOGGER_THREAD')
172-
def test_error_type_in_db_payload(self, mock_thread, mock_resolve):
146+
def test_error_type_excluded_from_db_payload(self, mock_thread, mock_resolve):
173147
mock_resolve.return_value.namespace = None
174148
mock_resolve.return_value.url_name = 'test'
175149
mock_thread.put_log_data = Mock()
@@ -183,39 +157,11 @@ def get_response(request):
183157

184158
mock_thread.put_log_data.assert_called_once()
185159
call_data = mock_thread.put_log_data.call_args[1]['data']
186-
self.assertEqual(call_data['error_type'], 'Permission denied.')
187-
188-
189-
@override_settings(DRF_API_LOGGER_DATABASE=True)
190-
class TestErrorModelField(TestCase):
191-
192-
def test_create_log_with_error_type(self):
193-
from drf_api_logger.models import APILogsModel
194-
log = APILogsModel.objects.create(
195-
api='/api/test/', headers='{}', body='', method='GET',
196-
client_ip_address='127.0.0.1', response='{"detail": "Not found."}',
197-
status_code=404, execution_time=0.01, added_on=timezone.now(),
198-
error_type='Not found.',
199-
)
200-
self.assertEqual(log.error_type, 'Not found.')
201-
202-
def test_create_log_without_error_type(self):
203-
from drf_api_logger.models import APILogsModel
204-
log = APILogsModel.objects.create(
205-
api='/api/test/', headers='{}', body='', method='GET',
206-
client_ip_address='127.0.0.1', response='{}',
207-
status_code=200, execution_time=0.01, added_on=timezone.now(),
208-
)
209-
self.assertIsNone(log.error_type)
210-
211-
def test_error_type_indexed(self):
212-
from drf_api_logger.models import APILogsModel
213-
field = APILogsModel._meta.get_field('error_type')
214-
self.assertTrue(field.db_index)
160+
self.assertNotIn('error_type', call_data)
215161

216162

217163
@override_settings(DRF_API_LOGGER_DATABASE=True)
218-
class TestErrorGrouping(TestCase):
164+
class TestErrorGroupingByStatusCode(TestCase):
219165

220166
def setUp(self):
221167
from drf_api_logger.models import APILogsModel
@@ -227,28 +173,25 @@ def setUp(self):
227173
api='/api/users/', headers='{}', body='', method='GET',
228174
client_ip_address='127.0.0.1', response='{}',
229175
status_code=404, execution_time=0.01, added_on=now,
230-
error_type='Not found.',
231176
)
232177
for i in range(3):
233178
APILogsModel.objects.create(
234179
api='/api/orders/', headers='{}', body='', method='POST',
235180
client_ip_address='127.0.0.1', response='{}',
236181
status_code=400, execution_time=0.02, added_on=now,
237-
error_type='BadRequest',
238182
)
239183
for i in range(2):
240184
APILogsModel.objects.create(
241185
api='/api/users/', headers='{}', body='', method='POST',
242186
client_ip_address='127.0.0.1', response='{}',
243187
status_code=500, execution_time=1.5, added_on=now,
244-
error_type='InternalServerError',
245188
)
246189

247-
def test_group_by_endpoint(self):
190+
def test_group_by_endpoint_and_status(self):
248191
from django.db.models import Count
249192
groups = list(
250193
self.APILogsModel.objects.filter(status_code__gte=400)
251-
.values('api', 'status_code', 'error_type')
194+
.values('api', 'status_code')
252195
.annotate(count=Count('id'))
253196
.order_by('-count')
254197
)
@@ -257,22 +200,20 @@ def test_group_by_endpoint(self):
257200
self.assertEqual(groups[0]['status_code'], 404)
258201
self.assertEqual(groups[0]['count'], 5)
259202

260-
def test_group_by_error_type(self):
203+
def test_group_by_status_code(self):
261204
from django.db.models import Count
262205
groups = list(
263206
self.APILogsModel.objects.filter(status_code__gte=400)
264-
.values('error_type')
207+
.values('status_code')
265208
.annotate(count=Count('id'))
266209
.order_by('-count')
267210
)
268211
self.assertEqual(len(groups), 3)
269-
self.assertEqual(groups[0]['error_type'], 'Not found.')
270212
self.assertEqual(groups[0]['count'], 5)
271213

272214
def test_error_rate(self):
273215
total = self.APILogsModel.objects.count()
274216
errors = self.APILogsModel.objects.filter(status_code__gte=400).count()
275217
rate = round((errors / total) * 100, 1)
276218
self.assertEqual(errors, 10)
277-
self.assertEqual(total, 10)
278219
self.assertEqual(rate, 100.0)

0 commit comments

Comments
 (0)