diff --git a/apps/graduation/views.py b/apps/graduation/views.py index 703730170..5760a9a82 100644 --- a/apps/graduation/views.py +++ b/apps/graduation/views.py @@ -15,4 +15,4 @@ def get(self, request): 'major': [gt.to_json() for gt in MajorTrack.objects.all().order_by('department__code', 'start_year', 'end_year')], 'additional': [gt.to_json() for gt in AdditionalTrack.objects.all().order_by(Length('type'), 'department__code', 'start_year', 'end_year')], } - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) diff --git a/apps/main/views.py b/apps/main/views.py index 3c73bc864..4fc1a2481 100644 --- a/apps/main/views.py +++ b/apps/main/views.py @@ -64,4 +64,4 @@ def get(self, request, user_id): feeds = [f for f in feeds if f is not None] feeds = sorted(feeds, key=(lambda f: f.priority)) result = [f.to_json(user=request.user) for f in feeds] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) diff --git a/apps/planner/views.py b/apps/planner/views.py index 34625ff3c..35fd3bad1 100644 --- a/apps/planner/views.py +++ b/apps/planner/views.py @@ -38,7 +38,7 @@ def get(self, request, user_id): planners = apply_order(planners, order, DEFAULT_ORDER) planners = apply_offset_and_limit(planners, offset, limit, MAX_LIMIT) result = [p.to_json() for p in planners] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) def post(self, request, user_id): BODY_STRUCTURE = [ @@ -107,7 +107,7 @@ def post(self, request, user_id): type=target_item.type, type_en=target_item.type_en, credit=target_item.credit, credit_au=target_item.credit_au) - return JsonResponse(planner.to_json()) + return JsonResponse(planner.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -122,7 +122,7 @@ def get(self, request, user_id, planner_id): except Planner.DoesNotExist: return HttpResponseNotFound() - return JsonResponse(planner.to_json()) + return JsonResponse(planner.to_json(),json_dumps_params={'ensure_ascii': False}) def patch(self, request, user_id, planner_id): BODY_STRUCTURE = [ @@ -162,7 +162,7 @@ def patch(self, request, user_id, planner_id): planner.taken_items.exclude(lecture__year__gte=start_year, lecture__year__lte=end_year).delete() planner.future_items.exclude(year__gte=start_year, year__lte=end_year).delete() planner.arbitrary_items.exclude(year__gte=start_year, year__lte=end_year).delete() - return JsonResponse(planner.to_json(), safe=False) + return JsonResponse(planner.to_json(), safe=False,json_dumps_params={'ensure_ascii': False}) def delete(self, request, user_id, planner_id): userprofile = request.user.userprofile @@ -207,7 +207,7 @@ def post(self, request, user_id, planner_id): return HttpResponseBadRequest("Wrong field 'course' in request data") item = FuturePlannerItem.objects.create(planner=planner, year=year, semester=semester, course=course) - return JsonResponse(item.to_json()) + return JsonResponse(item.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -242,7 +242,7 @@ def post(self, request, user_id, planner_id): item = ArbitraryPlannerItem.objects.create(planner=planner, year=year, semester=semester, department=department, type=type_, type_en=type_en, credit=credit, credit_au=credit_au) - return JsonResponse(item.to_json()) + return JsonResponse(item.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -286,7 +286,7 @@ def post(self, request, user_id, planner_id): if is_excluded is not None: target_item.is_excluded = is_excluded target_item.save() - return JsonResponse(target_item.to_json()) + return JsonResponse(target_item.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -321,7 +321,7 @@ def post(self, request, user_id, planner_id): except ArbitraryPlannerItem.DoesNotExist: HttpResponseBadRequest("No such planner item") target_item.delete() - return JsonResponse(planner.to_json()) + return JsonResponse(planner.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -343,4 +343,4 @@ def post(self, request, user_id, planner_id): arrange_order, = parse_body(request.body, BODY_STRUCTURE) reorder_planner(planner, arrange_order) - return JsonResponse(planner.to_json()) + return JsonResponse(planner.to_json(),json_dumps_params={'ensure_ascii': False}) diff --git a/apps/review/views.py b/apps/review/views.py index 29624e356..8b37e2ddd 100644 --- a/apps/review/views.py +++ b/apps/review/views.py @@ -45,7 +45,7 @@ def get(self, request): reviews = apply_order(reviews, order, DEFAULT_ORDER) reviews = apply_offset_and_limit(reviews, offset, limit, MAX_LIMIT) result = [r.to_json(user=request.user) for r in reviews] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) def post(self, request): BODY_STRUCTURE = [ @@ -75,14 +75,14 @@ def post(self, request): speech=speech, writer=user_profile, ) - return JsonResponse(review.to_json(user=request.user), safe=False) + return JsonResponse(review.to_json(user=request.user), safe=False,json_dumps_params={'ensure_ascii': False}) class ReviewInstanceView(View): def get(self, request, review_id): review = get_object_or_404(Review, id=review_id) result = review.to_json(user=request.user) - return JsonResponse(result) + return JsonResponse(result,json_dumps_params={'ensure_ascii': False}) def patch(self, request, review_id): BODY_STRUCTURE = [ @@ -114,7 +114,7 @@ def patch(self, request, review_id): "speech": speech, }, ) - return JsonResponse(review.to_json(user=request.user), safe=False) + return JsonResponse(review.to_json(user=request.user), safe=False,json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -161,4 +161,4 @@ def get(self, request, user_id): reviews = apply_order(reviews, order, DEFAULT_ORDER) reviews = apply_offset_and_limit(reviews, offset, limit, MAX_LIMIT) result = [r.to_json(user=request.user) for r in reviews] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) diff --git a/apps/session/views.py b/apps/session/views.py index a7c1aff44..69cc2f4e8 100644 --- a/apps/session/views.py +++ b/apps/session/views.py @@ -94,9 +94,10 @@ def login_callback(request): code = request.GET.get("code") sso_profile = sso_client.get_user_info(code) username = sso_profile["sid"] + email = sso_profile["email"] try: - user = User.objects.get(username=username) + user = User.objects.filter(email=email).order_by('-last_login').first() except User.DoesNotExist: user = None @@ -176,7 +177,7 @@ def department_options(request): json_encode_list(deps_other), ] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False, json_dumps_params={'ensure_ascii': False}) @login_required_ajax @@ -217,7 +218,7 @@ def unregister(request): user.delete() logout(request) - return JsonResponse(status=200, data={}) + return JsonResponse(status=200, data={},json_dumps_params={'ensure_ascii': False}) @login_required_ajax @@ -237,4 +238,4 @@ def info(request): "my_timetable_lectures": json_encode_list(profile.taken_lectures.exclude(Lecture.get_query_for_research())), "reviews": json_encode_list(profile.reviews.all()), } - return JsonResponse(ctx, safe=False) + return JsonResponse(ctx, safe=False,json_dumps_params={'ensure_ascii': False}) diff --git a/apps/subject/views.py b/apps/subject/views.py index 47bd8050d..d6848653e 100644 --- a/apps/subject/views.py +++ b/apps/subject/views.py @@ -25,7 +25,7 @@ def get(self, request): semesters = apply_order(semesters, order, DEFAULT_ORDER) result = [semester.to_json() for semester in semesters] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) class CourseListView(View): @@ -67,7 +67,7 @@ def get(self, request): courses = apply_order(courses, order, DEFAULT_ORDER) courses = apply_offset_and_limit(courses, offset, limit, MAX_LIMIT) result = [c.to_json(user=request.user) for c in courses] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False, json_dumps_params={'ensure_ascii': False}) class CourseInstanceView(View): @@ -75,7 +75,7 @@ def get(self, request, course_id): course = get_object_or_404(Course, id=course_id) result = course.to_json(user=request.user) - return JsonResponse(result) + return JsonResponse(result,json_dumps_params={'ensure_ascii': False}) class CourseListAutocompleteView(View): @@ -92,7 +92,7 @@ def get(self, request): match = services.match_autocomplete(keyword, courses, professors) if not match: return JsonResponse(keyword, safe=False) - return JsonResponse(match, safe=False) + return JsonResponse(match, safe=False,json_dumps_params={'ensure_ascii': False}) class CourseInstanceReviewsView(View): @@ -114,7 +114,7 @@ def get(self, request, course_id): reviews = apply_order(reviews, order, DEFAULT_ORDER) reviews = apply_offset_and_limit(reviews, offset, limit, MAX_LIMIT) result = [review.to_json(user=request.user) for review in reviews] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) class CourseInstanceLecturesView(View): @@ -131,7 +131,7 @@ def get(self, request, course_id): lectures = apply_order(lectures, order, DEFAULT_ORDER) result = [lecture.to_json() for lecture in lectures] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -190,7 +190,7 @@ def get(self, request): lectures = apply_order(lectures, order, DEFAULT_ORDER) lectures = apply_offset_and_limit(lectures, offset, limit, MAX_LIMIT) result = [lecture.to_json(nested=False) for lecture in lectures] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) class LectureInstanceView(View): @@ -198,7 +198,7 @@ def get(self, request, lecture_id): lecture = get_object_or_404(Lecture, id=lecture_id) result = lecture.to_json() - return JsonResponse(result) + return JsonResponse(result,json_dumps_params={'ensure_ascii': False}) class LectureListAutocompleteView(View): @@ -219,7 +219,7 @@ def get(self, request): match = services.match_autocomplete(keyword, lectures, professors) if not match: return JsonResponse(keyword, safe=False) - return JsonResponse(match, safe=False) + return JsonResponse(match, safe=False,json_dumps_params={'ensure_ascii': False}) class LectureInstanceReviewsView(View): @@ -240,7 +240,7 @@ def get(self, request, lecture_id): reviews = apply_order(reviews, order, DEFAULT_ORDER) reviews = apply_offset_and_limit(reviews, offset, limit, MAX_LIMIT) result = [review.to_json() for review in reviews] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) class LectureInstanceRelatedReviewsView(View): @@ -264,7 +264,7 @@ def get(self, request, lecture_id): reviews = apply_order(reviews, order, DEFAULT_ORDER) reviews = apply_offset_and_limit(reviews, offset, limit, MAX_LIMIT) result = [review.to_json() for review in reviews] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -284,4 +284,4 @@ def get(self, request, user_id): courses = apply_order(courses, order, DEFAULT_ORDER) result = [course.to_json(user=request.user) for course in courses] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) diff --git a/apps/support/views.py b/apps/support/views.py index cd6e57d84..8f35d43f7 100644 --- a/apps/support/views.py +++ b/apps/support/views.py @@ -29,7 +29,7 @@ def get(self, request): notices = apply_order(notices, order, DEFAULT_ORDER) result = [n.to_json() for n in notices] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") diff --git a/apps/timetable/views.py b/apps/timetable/views.py index d57f5d5a7..0e9ab4d13 100644 --- a/apps/timetable/views.py +++ b/apps/timetable/views.py @@ -49,7 +49,7 @@ def get(self, request, user_id): timetables = apply_order(timetables, order, DEFAULT_ORDER) timetables = apply_offset_and_limit(timetables, offset, limit, MAX_LIMIT) result = [t.to_json() for t in timetables] - return JsonResponse(result, safe=False) + return JsonResponse(result, safe=False,json_dumps_params={'ensure_ascii': False}) def post(self, request, user_id): BODY_STRUCTURE = [ @@ -82,7 +82,7 @@ def post(self, request, user_id): return HttpResponseBadRequest("Wrong field 'lectures' in request data") timetable.lectures.add(lecture) - return JsonResponse(timetable.to_json()) + return JsonResponse(timetable.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -97,7 +97,7 @@ def get(self, request, user_id, timetable_id): except Timetable.DoesNotExist: return HttpResponseNotFound() - return JsonResponse(timetable.to_json()) + return JsonResponse(timetable.to_json(),json_dumps_params={'ensure_ascii': False}) def delete(self, request, user_id, timetable_id): userprofile = request.user.userprofile @@ -147,7 +147,7 @@ def post(self, request, user_id, timetable_id): return HttpResponseBadRequest('Wrong field \'lecture\' in request data') timetable.lectures.add(lecture) - return JsonResponse(timetable.to_json()) + return JsonResponse(timetable.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -174,7 +174,7 @@ def post(self, request, user_id, timetable_id): lecture = Lecture.objects.get(id=lecture_id) timetable.lectures.remove(lecture) - return JsonResponse(timetable.to_json()) + return JsonResponse(timetable.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -196,7 +196,7 @@ def post(self, request, user_id, timetable_id): arrange_order, = parse_body(request.body, BODY_STRUCTURE) reorder_timetable(timetable, arrange_order) - return JsonResponse(timetable.to_json()) + return JsonResponse(timetable.to_json(),json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -209,7 +209,7 @@ def get(self, request, user_id): wishlist = Wishlist.objects.get_or_create(user=userprofile)[0] result = wishlist.to_json() - return JsonResponse(result) + return JsonResponse(result,json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -235,7 +235,7 @@ def post(self, request, user_id): wishlist.lectures.add(lecture) result = wishlist.to_json() - return JsonResponse(result) + return JsonResponse(result,json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") @@ -261,7 +261,7 @@ def post(self, request, user_id): wishlist.lectures.remove(lecture) result = wishlist.to_json() - return JsonResponse(result) + return JsonResponse(result,json_dumps_params={'ensure_ascii': False}) @method_decorator(login_required_ajax, name="dispatch") diff --git a/docker-compose.yml b/docker-compose.yml index 1e34a04ba..b439508c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: ports: - "58000:8000" volumes: - - .:/var/www/otlplus:ro + - .:/var/www/otlplus working_dir: /var/www/otlplus wait-for-db: image: atkrad/wait4x diff --git a/docker/Dockerfile.back b/docker/Dockerfile.back index 135f1a85c..930efd96b 100644 --- a/docker/Dockerfile.back +++ b/docker/Dockerfile.back @@ -17,9 +17,9 @@ ADD . . EXPOSE 8000 -ADD ./volumes/config /root/.ssh/config -ADD ./volumes/key.pem /root/key.pem -ADD ./volumes/wheel-2021.pem /root/wheel-2021.pem -RUN chown -R root:root /root && chmod 400 /root/key.pem && chmod 400 /root/wheel-2021.pem && echo "StrictHostKeyChecking no" >> /etc/ssh_config +#ADD ./volumes/config /root/.ssh/config +#ADD ./volumes/key.pem /root/key.pem +#ADD ./volumes/wheel-2021.pem /root/wheel-2021.pem +#RUN chown -R root:root /root && chmod 400 /root/key.pem && chmod 400 /root/wheel-2021.pem && echo "StrictHostKeyChecking no" >> /etc/ssh_config CMD ["gunicorn", "otlplus.wsgi", "--bind", "0.0.0.0:8000", "--log-file", "-", "--workers", "6", "--threads", "12", "--worker-class", "gevent"] diff --git a/logs/__init__.py b/logs/__init__.py new file mode 100644 index 000000000..11310bf7a --- /dev/null +++ b/logs/__init__.py @@ -0,0 +1,3 @@ +import logging + +log = logging.getLogger("otlplus_logger") \ No newline at end of file diff --git a/logs/handler.py b/logs/handler.py new file mode 100644 index 000000000..523b57107 --- /dev/null +++ b/logs/handler.py @@ -0,0 +1,148 @@ +import json +import logging +import os +import time +import uuid +from collections import OrderedDict +from datetime import datetime +from logging import handlers + +from logs.log_object import ErrorLogObject + + +class LogMiddlewareHandler(logging.Handler): + @staticmethod + def message_from_record(record): + if ( + isinstance(record.msg, dict) + or isinstance(record.msg, str) + or isinstance(record.msg, int) + ): + message = {"raw": record.msg} + elif isinstance(record.msg, Exception): + message = ErrorLogObject.format_exception(record.msg) + else: + message = record.msg.format() + return message + + def format(self, record): + message = self.message_from_record(record) + return json.dumps( + OrderedDict( + [ + *message.items(), + ] + ), ensure_ascii=False + ) + + +class ConsoleHandler(logging.StreamHandler, LogMiddlewareHandler): + pass + + +class FileHandler(handlers.TimedRotatingFileHandler, LogMiddlewareHandler): + pass + + +class SizedTimedRotatingFileHandler( + handlers.TimedRotatingFileHandler, LogMiddlewareHandler +): + """ + Handler for logging to a set of files, which switches from one file + to the next when the current file reaches a certain size, or at certain + timed intervals + """ + + def __init__( + self, + filename, + max_bytes=0, + backup_count=0, + encoding=None, + delay=0, + when="h", + interval=1, + utc=False, + ): + handlers.TimedRotatingFileHandler.__init__( + self, filename, when, interval, backup_count, encoding, delay, utc + ) + self.maxBytes = max_bytes + + def shouldRollover(self, record) -> int: + """ + Determine if rollover should occur. + + Basically, see if the supplied record would cause the file to exceed + the size limit we have. + """ + if self.stream is None: + self.stream = self._open() + if self.maxBytes > 0: + msg = "%s\n" % self.format(record) + # due to non-posix-compliant Windows feature + self.stream.seek(0, 2) + if self.stream.tell() + len(msg) >= self.maxBytes: + return 1 + t = int(time.time()) + if t >= self.rolloverAt: + return 1 + return 0 + + + def doRollover(self): + """ + do a rollover; in this case, a date/time stamp is appended to the filename + when the rollover happens. However, you want the file to be named for the + start of the interval, not the current time. If there is a backup count, + then we have to get a list of matching filenames, sort them and remove + the one with the oldest suffix. + """ + if self.stream: + self.stream.close() + self.stream = None + # get the time that this sequence started at and make it a TimeTuple + currentTime = int(time.time()) + dstNow = time.localtime(currentTime)[-1] + t = self.rolloverAt - self.interval + self.baseFilename = os.path.abspath(os.fspath(os.path.join('/var/www/otlplus/logs/', f'response-{datetime.now().strftime("%Y-%m-%d")}.log'))) + if self.utc: + timeTuple = time.gmtime(t) + else: + timeTuple = time.localtime(t) + dstThen = timeTuple[-1] + if dstNow != dstThen: + if dstNow: + addend = 3600 + else: + addend = -3600 + timeTuple = time.localtime(t + addend) + dfn = self.rotation_filename(self.baseFilename + "." + + time.strftime(self.suffix, timeTuple)) + if os.path.exists(dfn): + os.remove(dfn) + self.rotate(self.baseFilename, dfn) + if self.backupCount > 0: + for s in self.getFilesToDelete(): + os.remove(s) + if not self.delay: + self.stream = self._open() + newRolloverAt = self.computeRollover(currentTime) + while newRolloverAt <= currentTime: + newRolloverAt = newRolloverAt + self.interval + #If DST changes and midnight or weekly rollover, adjust for this. + if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc: + dstAtRollover = time.localtime(newRolloverAt)[-1] + if dstNow != dstAtRollover: + if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour + addend = -3600 + else: # DST bows out before next rollover, so we need to add an hour + addend = 3600 + newRolloverAt += addend + self.rolloverAt = newRolloverAt + + def exitRollover(self): + self.delay = True + if self.stream: + if self.stream.tell() > 0: + self.doRollover() \ No newline at end of file diff --git a/logs/log_object.py b/logs/log_object.py new file mode 100644 index 000000000..7ae137bd4 --- /dev/null +++ b/logs/log_object.py @@ -0,0 +1,130 @@ +import json +import abc +import logging +import traceback + +REQUEST_META_KEYS = [ + "PATH_INFO", + "HTTP_X_SCHEME", + "REMOTE_ADDR", + "TZ", + "REMOTE_HOST", + "CONTENT_TYPE", + "CONTENT_LENGTH", + "HTTP_AUTHORIZATION", + "HTTP_HOST", + "HTTP_USER_AGENT", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_REAL_IP", + "HTTP_X_REQUEST_ID", +] + + +class BaseLogObject(metaclass=abc.ABCMeta): + def __init__(self, request): + self.request = request + + def format_request(self) -> dict: + # print(self.request.META['HTTP_UUID']) + # print(self.request.META) + result = { + "method": self.request.method, + # "meta": { + # key.lower(): str(value) + # for key, value in self.request.META.items() + # if key in REQUEST_META_KEYS + # }, + "UUID": self.request.META['HTTP_UUID'] if self.request.META['HTTP_UUID'] else None, + "path": self.request.path_info, + } + + try: + result["data"] = {key: value for key, value in self.request.data.items()} + except AttributeError: + if self.request.method == "GET": + result["data"] = self.request.GET.dict() + elif self.request.method == "POST": + result["data"] = self.request.POST.dict() + + try: + result["user"] = self.request.user.username + except AttributeError: + result["user"] = None + + return result + + @abc.abstractmethod + def format(self): + pass + + +import json + +class LogObject(BaseLogObject): + def __init__(self, request, response): + super(LogObject, self).__init__(request) + self.response = response + + def format(self) -> dict: + return { + "request": self.format_request(), + "response": self.format_response(), + "duration": self.duration + } + + def format_response(self) -> dict: + result = { + "status": self.response.status_code, + # "headers": dict(self.response.items()), + # "charset": getattr(self.response, "charset", 'utf-8'), + } + + try: + # If the response content is JSON, parse it correctly and avoid double escaping + if isinstance(self.response.content, bytes): + # Decode and load the JSON data + result["data"] = json.loads(self.response.content.decode(self.response.charset or 'utf-8')) + else: + result["data"] = self.response.content + except json.JSONDecodeError: + # If not JSON, log as plain text + result["data"] = self.response.content.decode(self.response.charset or 'utf-8') + + return result + + def __repr__(self): + # Use ensure_ascii=False to properly handle non-ASCII characters in the logs + return json.dumps(self.format(), ensure_ascii=False) + + +class ErrorLogObject(BaseLogObject): + def __init__(self, request, exception): + super(ErrorLogObject, self).__init__(request) + self.exception = exception + + def format(self) -> dict: + return { + "request": self.format_request(), + "exception": self.format_exception(self.exception), + } + + @staticmethod + def exception_type(exception) -> str: + return str(type(exception)).split("'")[1] + + @staticmethod + def format_exception(exception) -> dict: + tb = [ + {"file": item[0], "line": item[1], "method": item[2]} + for item in traceback.extract_tb( + traceback.TracebackException.from_exception(exception).exc_traceback + ) + ] + return { + "message": str(exception), + "type": ErrorLogObject.exception_type(exception), + "traceback": tb, + } + + def __repr__(self): + return str(self.format()) diff --git a/logs/middleware.py b/logs/middleware.py new file mode 100644 index 000000000..f6493b1c7 --- /dev/null +++ b/logs/middleware.py @@ -0,0 +1,30 @@ +from datetime import timedelta +import time + +from . import log +from .handler import ConsoleHandler +from .log_object import ErrorLogObject, LogObject + + +class LoggingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + start = time.monotonic() + response = self.get_response(request) + end = time.monotonic() + duration = (end - start) * 1000 # 밀리초 단위로 변환 + log_data = LogObject(request, response) + log_data.duration = duration + if response.status_code == 500: + return response + if 400 <= response.status_code < 500: + log.warning(log_data) + else: + log.info(log_data) + return response + + @staticmethod + def process_exception(request, exception): + log.error(ErrorLogObject(request, exception)) \ No newline at end of file diff --git a/logs/response.log.2024-08-26 b/logs/response.log.2024-08-26 new file mode 100644 index 000000000..e69de29bb diff --git a/otlplus/settings.py b/otlplus/settings.py index e671392f7..66c6ebf5f 100644 --- a/otlplus/settings.py +++ b/otlplus/settings.py @@ -12,6 +12,7 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os +from datetime import datetime BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -60,6 +61,8 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "logs.middleware.LoggingMiddleware", + ] ROOT_URLCONF = "otlplus.urls" @@ -111,6 +114,56 @@ }, } + +import os +import sys + +LOG_FILE_PATH = os.environ.get("LOG_FILE_PATH", "/otl-django/logs/") +LOG_MAX_BYTES = int(os.environ.get("LOG_MAX_BYTES", 1024 * 1024 * 10)) +LOG_BACKUP_COUNT = int(os.environ.get("LOG_BACKUP_COUNT", 100)) + +LOGGING = { + "version": 1, + "disable_existing_loggers": True, + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "formatters": { + "standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"} + }, + "handlers": { + "default": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "standard", + }, + "console": { + "level": "INFO", + "class": "logs.handler.ConsoleHandler", + "stream": sys.stdout, + }, + "rotating_file": { + "level": "INFO", + "class": "logs.handler.SizedTimedRotatingFileHandler", + "filename": os.path.join('logs/', f'response-{datetime.now().strftime("%Y-%m-%d")}.log'), + "max_bytes": LOG_MAX_BYTES, + "backup_count": LOG_BACKUP_COUNT, + "encoding": "utf-8", + "when": "midnight", + }, + }, + "loggers": { + "default": { + "handlers": ["default"], + "level": "DEBUG", + "propagate": True, + }, + "otlplus_logger": { + "handlers": ["rotating_file", "console"], + "level": "DEBUG", + "propagate": False, + }, + }, +} + # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ @@ -151,8 +204,11 @@ def ugettext(s): with open(os.path.join(BASE_DIR, "keys/sso_secret")) as f: SSO_SECRET_KEY = f.read().strip() -SSO_CLIENT_ID = os.getenv("SSO_CLIENT_ID") -SSO_IS_BETA = DEBUG +# SSO_CLIENT_ID = os.getenv("SSO_CLIENT_ID") +SSO_CLIENT_ID = "otlplus" # SSO의 'Name' (또는 'Client ID') 필드 +SSO_SECRET_KEY = "d980d9421fb5abe202a5" # SSO의 'Secret Key' 필드 + +SSO_IS_BETA = False LOGIN_URL = "/session/login/" LOGOUT_URL = "/session/logout/" diff --git a/otlplus/urls.py b/otlplus/urls.py index 58da6ae24..0e9cf045c 100644 --- a/otlplus/urls.py +++ b/otlplus/urls.py @@ -145,6 +145,6 @@ # url(r"^api/external/google/google_auth_return$", # timetable_views.external_google_google_auth_return_view), - url(r"^api/status$", lambda request: HttpResponse()), + url(r"^api/status$", lambda request: HttpResponse("I am healthy")), url(r"^api/", lambda request: HttpResponseNotFound("Bad url")), ] diff --git a/scholardb_access.py b/scholardb_access.py index 54da4cf4f..97ee220f8 100755 --- a/scholardb_access.py +++ b/scholardb_access.py @@ -21,7 +21,7 @@ def execute(host, port, user, password, query): os.system("scp /tmp/otl_db_ssh_args xen:/tmp > /dev/null") os.remove("/tmp/otl_db_ssh_args") - os.system("ssh xen python db.py > /dev/null") + os.system("ssh xen python newdb/db.py > /dev/null") os.system("scp xen:/tmp/otl_db_dump_result /tmp > /dev/null") os.system("ssh xen rm /tmp/otl_db_dump_result > /dev/null") result = pickle.load(open("/tmp/otl_db_dump_result", "rb"), encoding="bytes") diff --git a/utils/middleware.py b/utils/middleware.py index 926e4307b..57860f250 100644 --- a/utils/middleware.py +++ b/utils/middleware.py @@ -18,7 +18,7 @@ def process_request(self, request): assert hasattr( request, "session", - ), "Cached authentication middleware requires Session middleware to work correctly." + ), "Cached authentication middleware.py requires Session middleware.py to work correctly." try: key = "cached-user:%d" % request.session[SESSION_KEY] except KeyError: