feat: v2 error and debug log structure

This commit is contained in:
Ankush Menat 2023-09-18 15:50:31 +05:30
parent 6fd97bcbcf
commit e2714c3e1c
3 changed files with 63 additions and 24 deletions

View file

@ -488,9 +488,12 @@ def msgprint(
def _raise_exception(): def _raise_exception():
if raise_exception: if raise_exception:
if inspect.isclass(raise_exception) and issubclass(raise_exception, Exception): if inspect.isclass(raise_exception) and issubclass(raise_exception, Exception):
raise raise_exception(msg) exc = raise_exception(msg)
else: else:
raise ValidationError(msg) exc = ValidationError(msg)
if out.__frappe_exc_id:
exc.__frappe_exc_id = out.__frappe_exc_id
raise exc
if flags.mute_messages: if flags.mute_messages:
_raise_exception() _raise_exception()
@ -527,6 +530,7 @@ def msgprint(
if raise_exception: if raise_exception:
out.raise_exception = 1 out.raise_exception = 1
out.__frappe_exc_id = generate_hash()
if primary_action: if primary_action:
out.primary_action = primary_action out.primary_action = primary_action
@ -535,10 +539,6 @@ def msgprint(
out.wide = wide out.wide = wide
message_log.append(out) message_log.append(out)
if raise_exception and hasattr(raise_exception, "__name__"):
local.response["exc_type"] = raise_exception.__name__
_raise_exception() _raise_exception()

View file

@ -200,7 +200,7 @@ class TestResourceAPI(FrappeAPITestCase):
self.assertIsInstance(json.data, list) self.assertIsInstance(json.data, list)
self.assertIsInstance(json.data[0], list) self.assertIsInstance(json.data[0], list)
@parameterize("", "v1", "v2") @parameterize("", "v1")
def test_get_list_debug(self): def test_get_list_debug(self):
# test 5: fetch response with debug # test 5: fetch response with debug
with suppress_stdout(): with suppress_stdout():
@ -253,12 +253,6 @@ class TestResourceAPI(FrappeAPITestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.GENERATED_DOCUMENTS.remove(doc_to_delete) self.GENERATED_DOCUMENTS.remove(doc_to_delete)
non_existent_doc = frappe.generate_hash(length=12)
with suppress_stdout():
response = self.delete(self.resource_path(self.DOCTYPE, non_existent_doc))
self.assertEqual(response.status_code, 404)
self.assertDictEqual(response.json, {})
@parameterize("", "v1") @parameterize("", "v1")
def test_run_doc_method(self): def test_run_doc_method(self):
# test 10: Run whitelisted method on doc via /api/resource # test 10: Run whitelisted method on doc via /api/resource
@ -372,6 +366,15 @@ class TestDocumentAPIV2(TestResourceAPI):
response = self.get(self.resource_path(self.DOCTYPE, random_doc)) response = self.get(self.resource_path(self.DOCTYPE, random_doc))
self.assertEqual(response.json["data"]["description"], generated_desc) self.assertEqual(response.json["data"]["description"], generated_desc)
def test_delete_document_non_existing(self):
non_existent_doc = frappe.generate_hash(length=12)
with suppress_stdout():
response = self.delete(self.resource_path(self.DOCTYPE, non_existent_doc))
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json["errors"][0]["type"], "DoesNotExistError")
# 404s dont return exceptions
self.assertFalse(response.json["errors"][0].get("exception"))
class TestMethodAPIV2(FrappeAPITestCase): class TestMethodAPIV2(FrappeAPITestCase):
version = "v2" version = "v2"
@ -479,7 +482,7 @@ class TestMethodAPIV2(FrappeAPITestCase):
response = self.get( response = self.get(
self.method_path(method), self.method_path(method),
{"sid": self.sid, "message": expected_message, "fail": True, "handled": False}, {"sid": self.sid, "message": expected_message, "fail": True, "handled": False},
) ).json
self.assertIsInstance(response["errors"], list) self.assertIsInstance(response["errors"], list)
self.assertEqual(response["errors"][0]["type"], "ZeroDivisionError") self.assertEqual(response["errors"][0]["type"], "ZeroDivisionError")
@ -522,7 +525,7 @@ class TestReadOnlyMode(FrappeAPITestCase):
self.assertIsInstance(response.json, dict) self.assertIsInstance(response.json, dict)
self.assertIsInstance(response.json["data"], list) self.assertIsInstance(response.json["data"], list)
@parameterize("", "v1", "v2") @parameterize("", "v1")
def test_blocked_writes(self): def test_blocked_writes(self):
with suppress_stdout(): with suppress_stdout():
response = self.post( response = self.post(
@ -531,6 +534,15 @@ class TestReadOnlyMode(FrappeAPITestCase):
self.assertEqual(response.status_code, 503) self.assertEqual(response.status_code, 503)
self.assertEqual(response.json["exc_type"], "InReadOnlyMode") self.assertEqual(response.json["exc_type"], "InReadOnlyMode")
@parameterize("v2")
def test_blocked_writes_v2(self):
with suppress_stdout():
response = self.post(
self.resource_path("ToDo"), {"description": frappe.mock("paragraph"), "sid": self.sid}
)
self.assertEqual(response.status_code, 503)
self.assertEqual(response.json["errors"][0]["type"], "InReadOnlyMode")
class TestWSGIApp(FrappeAPITestCase): class TestWSGIApp(FrappeAPITestCase):
def test_request_hooks(self): def test_request_hooks(self):

View file

@ -6,6 +6,7 @@ import decimal
import json import json
import mimetypes import mimetypes
import os import os
import sys
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
from urllib.parse import quote from urllib.parse import quote
@ -29,22 +30,45 @@ if TYPE_CHECKING:
def report_error(status_code): def report_error(status_code):
"""Build error. Show traceback in developer mode""" """Build error. Show traceback in developer mode"""
allow_traceback = frappe.get_system_settings("allow_error_traceback") if frappe.db else False from frappe.api import ApiVersion, get_api_version
if (
allow_traceback allow_traceback = (
and (status_code != 404 or frappe.conf.logging) (frappe.get_system_settings("allow_error_traceback") if frappe.db else False)
and not frappe.local.flags.disable_traceback and not frappe.local.flags.disable_traceback
): and (status_code != 404 or frappe.conf.logging)
traceback = frappe.utils.get_traceback() )
if traceback:
frappe.errprint(traceback) traceback = frappe.utils.get_traceback()
frappe.local.response.exception = traceback.splitlines()[-1] exc_type, exc_value, _ = sys.exc_info()
match get_api_version():
case ApiVersion.V1:
if allow_traceback:
frappe.errprint(traceback)
frappe.response.exception = traceback.splitlines()[-1]
frappe.response["exc_type"] = exc_type.__name__
case ApiVersion.V2:
error_log = {"type": exc_type.__name__}
if allow_traceback:
error_log["exception"] = traceback
_link_error_with_message_log(error_log, exc_value, frappe.message_log)
frappe.local.response.errors = [error_log]
response = build_response("json") response = build_response("json")
response.status_code = status_code response.status_code = status_code
return response return response
def _link_error_with_message_log(error_log, exception, message_logs):
for message in message_logs:
if message.get("__frappe_exc_id") == exception.__frappe_exc_id:
error_log.update(message)
message_logs.remove(message)
error_log.pop("raise_exception", None)
error_log.pop("__frappe_exc_id", None)
return
def build_response(response_type=None): def build_response(response_type=None):
if "docs" in frappe.local.response and not frappe.local.response.docs: if "docs" in frappe.local.response and not frappe.local.response.docs:
del frappe.local.response["docs"] del frappe.local.response["docs"]
@ -164,6 +188,9 @@ def _make_logs_v2():
if frappe.local.message_log: if frappe.local.message_log:
response["messages"] = frappe.local.message_log response["messages"] = frappe.local.message_log
if frappe.debug_log and frappe.conf.get("logging"):
response["debug"] = [{"message": m} for m in frappe.local.debug_log]
def json_handler(obj): def json_handler(obj):
"""serialize non-serializable data for json""" """serialize non-serializable data for json"""