Skip to content

Commit 3833746

Browse files
author
Myth
committed
first commit
0 parents  commit 3833746

File tree

15 files changed

+720
-0
lines changed

15 files changed

+720
-0
lines changed

.gitignore

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Byte-compiled / optimized / DLL files
2+
.idea/
3+
__pycache__/
4+
*.py[cod]
5+
*$py.class
6+
7+
# C extensions
8+
*.so
9+
10+
# Distribution / packaging
11+
.Python
12+
env/
13+
build/
14+
develop-eggs/
15+
dist/
16+
downloads/
17+
eggs/
18+
.eggs/
19+
lib/
20+
lib64/
21+
parts/
22+
sdist/
23+
var/
24+
*.egg-info/
25+
.installed.cfg
26+
*.egg
27+
28+
# PyInstaller
29+
# Usually these files are written by a python script from a template
30+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
31+
*.manifest
32+
*.spec
33+
34+
# Installer logs
35+
pip-log.txt
36+
pip-delete-this-directory.txt
37+
38+
# Unit test / coverage reports
39+
htmlcov/
40+
.tox/
41+
.coverage
42+
.coverage.*
43+
.cache
44+
nosetests.xml
45+
coverage.xml
46+
*,cover
47+
.hypothesis/
48+
49+
# Translations
50+
*.mo
51+
*.pot
52+
53+
# Django stuff:
54+
*.log
55+
local_settings.py
56+
57+
# Flask stuff:
58+
instance/
59+
.webassets-cache
60+
61+
# Scrapy stuff:
62+
.scrapy
63+
64+
# Sphinx documentation
65+
docs/_build/
66+
67+
# PyBuilder
68+
target/
69+
70+
# IPython Notebook
71+
.ipynb_checkpoints
72+
73+
# pyenv
74+
.python-version
75+
76+
# celery beat schedule file
77+
celerybeat-schedule
78+
79+
# dotenv
80+
.env
81+
82+
# virtualenv
83+
venv/
84+
ENV/
85+
86+
# Spyder project settings
87+
.spyderproject
88+
89+
# Rope project settings
90+
.ropeproject

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Django API Log
2+
==============
3+
4+
Django reusable app for record request/response information to database for alert and audit.
5+
6+
7+
### Installation
8+
`pip install django_api_log`
9+
10+
### Usage.
11+
12+
1. add `django_api_log.apps.DjangoApiLogConfig` to INSTALLED_APPS.
13+
2. add `django_api_log.middleware.ApiLogMiddleware` to MIDDLEWARE (MIDDLEWARE_CLASSES if you use django<1.10)
14+
3. run `python manage.py migrate`
15+
4. config urlpatterns
16+
eg: add `url(r'^api-log', include(u'django_api_log.urls'), name='django_api_log')` to your root urlpatterns
17+
or other urlpatterns you like .
18+
19+
then, everything should be fine. :)
20+
21+
### Query saved logs
22+
open your browser then visit YOUR_API_HOST/api-log (or use the path you use)

django_api_log/__init__.py

Whitespace-only changes.

django_api_log/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Register your models here.
2+
import corsheaders

django_api_log/apps.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from __future__ import unicode_literals
2+
3+
from django.apps import AppConfig
4+
5+
6+
class DjangoApiLogConfig(AppConfig):
7+
name = u'django_api_log'

django_api_log/middleware.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# -*- coding:utf8 -*-
2+
"""
3+
Author : Myth
4+
Date : 16/11/24
5+
Email : belongmyth at 163.com
6+
"""
7+
8+
import json
9+
import logging
10+
import traceback
11+
12+
from datetime import datetime
13+
from django.urls import resolve, Resolver404
14+
15+
from .models import ApiLog
16+
from .settings import settings
17+
18+
logging.basicConfig()
19+
logger = logging.getLogger(u'django-request')
20+
21+
UTF_8 = str('utf-8')
22+
23+
24+
def retrieve_headers(META):
25+
headers = {}
26+
for key, value in META.items():
27+
if not isinstance(value, basestring):
28+
continue
29+
if key in (u'CONTENT_LENGTH', u'CONTENT_TYPE', u'REMOTE_ADDR'):
30+
headers[key] = value
31+
continue
32+
if not key.startswith(u'HTTP_'):
33+
continue
34+
headers[key[5:]] = value
35+
return headers
36+
37+
38+
def utf8(content):
39+
if not content:
40+
return content
41+
return content # .decode(UTF_8)
42+
43+
44+
def get_client_ip(request):
45+
if 'HTTP_X_FORWARDED_FOR' in request.META:
46+
return request.META['HTTP_X_FORWARDED_FOR'].split(",")[0].strip()
47+
elif 'HTTP_X_REAL_IP' in request.META:
48+
return request.META['HTTP_X_REAL_IP']
49+
elif 'REMOTE_ADDR' in request.META:
50+
return request.META['REMOTE_ADDR']
51+
else:
52+
return 'ip-not-found'
53+
54+
55+
class ApiLogMiddleware(object):
56+
def __init__(self, get_response):
57+
self.get_response = get_response
58+
self.exception = None
59+
60+
def __call__(self, request):
61+
# start to timing response
62+
apilog = ApiLog()
63+
apilog.start_time = datetime.now()
64+
response = self.get_response(request)
65+
66+
no_log = getattr(request, u'__NO_LOG__', False)
67+
no_method_log = getattr(request, u'__NO_%s_LOG__' % request.method, False)
68+
69+
# 即使标记了忽略log的API, 如果http code >= 400 依然强制不忽略
70+
if (no_log or no_method_log) and response.status_code < 400:
71+
return response
72+
73+
# 默认忽略所有正确的 GET 请求记录
74+
if request.method == u'GET' and response.status_code < 400 and settings.IGNORE_RIGHT_GET:
75+
return response
76+
77+
try:
78+
apilog.client_ip = get_client_ip(request)
79+
apilog.method = request.method
80+
apilog.path = request.path
81+
apilog.raw_query = json.dumps(request.GET)
82+
try:
83+
apilog.raw_request_headers = json.dumps({u'headers': json.dumps(retrieve_headers(request.META))})
84+
except Exception as e:
85+
apilog.raw_request_headers = json.dumps({u'headers': u'get request headers failed: %s' % e})
86+
apilog.raw_request_body = json.dumps({u'body': utf8(request.body)})
87+
88+
apilog.http_code = response.status_code
89+
apilog.http_reason = response.reason_phrase
90+
try:
91+
apilog.raw_response_headers = json.dumps(
92+
{u'headers': json.dumps(dict([v for v in response._headers.values()]))})
93+
except Exception as e:
94+
apilog.raw_response_headers = json.dumps({u'headers': u'get response headers failed: %s' % e})
95+
96+
try:
97+
r = resolve(request.path)
98+
apilog.app_name = r.app_name
99+
apilog.url_name = r.url_name
100+
apilog.view_name = r.view_name
101+
apilog.func_name = r.func.func_name
102+
except Resolver404:
103+
apilog.app_name = u'__not_resolve__'
104+
apilog.url_name = u'__not_resolve__'
105+
apilog.view_name = u'__not_resolve__'
106+
apilog.func_name = u'__not_resolve__'
107+
108+
# only record http response body when response http code >= 400
109+
if response.status_code >= 400 or request.method != u'GET':
110+
apilog.raw_response_body = json.dumps({u'body': utf8(response.content)})
111+
112+
# for uncaught_exception
113+
uncaught_exception = getattr(request, 'uncaught_exception', None)
114+
if uncaught_exception:
115+
apilog.exception = str(uncaught_exception)
116+
apilog.traceback = getattr(request, 'uncaught_exception_format', '')
117+
118+
apilog.end_time = datetime.now()
119+
apilog.duration = round((apilog.end_time - apilog.start_time).total_seconds() * 1000, 2)
120+
apilog.save()
121+
try:
122+
if response.status_code >= 400:
123+
notify_func = settings.NOTIFY_FUNC
124+
if callable(notify_func):
125+
notify_func(apilog.json(request))
126+
except Exception:
127+
logger.exception(u'Error when notify api error:')
128+
except Exception:
129+
logger.exception(u'Error when save api log:')
130+
131+
return response
132+
133+
def process_exception(self, request, exception):
134+
request.uncaught_exception = exception
135+
request.uncaught_exception_format = traceback.format_exc()
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.10.1 on 2017-01-11 16:58
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
initial = True
11+
12+
dependencies = [
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='ApiLog',
18+
fields=[
19+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('created', models.DateTimeField(auto_now_add=True)),
21+
('client_ip', models.CharField(max_length=20)),
22+
('method', models.CharField(max_length=10)),
23+
('path', models.CharField(max_length=200)),
24+
('raw_query', models.TextField()),
25+
('raw_request_headers', models.TextField()),
26+
('raw_request_body', models.TextField()),
27+
('http_code', models.IntegerField()),
28+
('http_reason', models.CharField(max_length=100)),
29+
('raw_response_headers', models.TextField()),
30+
('raw_response_body', models.TextField()),
31+
('app_name', models.CharField(default='', max_length=100)),
32+
('url_name', models.CharField(default='', max_length=100)),
33+
('view_name', models.CharField(default='', max_length=100)),
34+
('func_name', models.CharField(default='', max_length=100)),
35+
('exception', models.CharField(max_length=100)),
36+
('traceback', models.TextField()),
37+
('start_time', models.DateTimeField(auto_now=True)),
38+
('end_time', models.DateTimeField(auto_now_add=True)),
39+
('duration', models.FloatField()),
40+
],
41+
options={
42+
'ordering': ['-created'],
43+
'abstract': False,
44+
},
45+
),
46+
]

django_api_log/migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)