From 1a3602f71530348bba04996eb77af8b23a6857cf Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 6 Jun 2025 13:22:25 +0530 Subject: [PATCH] feat: add in a doctype to optionally track API Requests (#32622) --- frappe/api/__init__.py | 11 ++++ .../core/doctype/api_request_log/__init__.py | 0 .../api_request_log/api_request_log.js | 8 +++ .../api_request_log/api_request_log.json | 62 +++++++++++++++++++ .../api_request_log/api_request_log.py | 28 +++++++++ .../api_request_log/test_api_request_log.py | 29 +++++++++ .../core/doctype/log_settings/log_settings.py | 1 + .../system_settings/system_settings.json | 17 ++++- .../system_settings/system_settings.py | 1 + frappe/hooks.py | 1 + 10 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 frappe/core/doctype/api_request_log/__init__.py create mode 100644 frappe/core/doctype/api_request_log/api_request_log.js create mode 100644 frappe/core/doctype/api_request_log/api_request_log.json create mode 100644 frappe/core/doctype/api_request_log/api_request_log.py create mode 100644 frappe/core/doctype/api_request_log/test_api_request_log.py diff --git a/frappe/api/__init__.py b/frappe/api/__init__.py index 5c504b2512..a8736db67e 100644 --- a/frappe/api/__init__.py +++ b/frappe/api/__init__.py @@ -41,6 +41,17 @@ def handle(request: Request): `DELETE` will delete """ + if frappe.get_system_settings("log_api_requests"): + doc = frappe.get_doc( + { + "doctype": "API Request Log", + "path": request.path, + "user": frappe.session.user, + "method": request.method, + } + ) + doc.deferred_insert() + try: endpoint, arguments = API_URL_MAP.bind_to_environ(request.environ).match() except NotFound: # Wrap 404 - backward compatiblity diff --git a/frappe/core/doctype/api_request_log/__init__.py b/frappe/core/doctype/api_request_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/api_request_log/api_request_log.js b/frappe/core/doctype/api_request_log/api_request_log.js new file mode 100644 index 0000000000..010cc02679 --- /dev/null +++ b/frappe/core/doctype/api_request_log/api_request_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("API Request Log", { +// refresh(frm) { + +// }, +// }); diff --git a/frappe/core/doctype/api_request_log/api_request_log.json b/frappe/core/doctype/api_request_log/api_request_log.json new file mode 100644 index 0000000000..834309a62f --- /dev/null +++ b/frappe/core/doctype/api_request_log/api_request_log.json @@ -0,0 +1,62 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-05-21 16:51:56.070193", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "path", + "method", + "user" + ], + "fields": [ + { + "fieldname": "path", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Path" + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "User", + "options": "User" + }, + { + "fieldname": "method", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Method" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-05-21 17:09:55.054044", + "modified_by": "Administrator", + "module": "Core", + "name": "API Request Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/frappe/core/doctype/api_request_log/api_request_log.py b/frappe/core/doctype/api_request_log/api_request_log.py new file mode 100644 index 0000000000..50ca6bf318 --- /dev/null +++ b/frappe/core/doctype/api_request_log/api_request_log.py @@ -0,0 +1,28 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class APIRequestLog(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + method: DF.Data | None + path: DF.Data | None + user: DF.Link | None + # end: auto-generated types + + @staticmethod + def clear_old_logs(days: int = 90): + from frappe.query_builder import Interval + from frappe.query_builder.functions import Now + + table = frappe.qb.DocType("API Request Log") + frappe.db.delete(table, filters=(table.creation < (Now() - Interval(days=days)))) diff --git a/frappe/core/doctype/api_request_log/test_api_request_log.py b/frappe/core/doctype/api_request_log/test_api_request_log.py new file mode 100644 index 0000000000..f61b335813 --- /dev/null +++ b/frappe/core/doctype/api_request_log/test_api_request_log.py @@ -0,0 +1,29 @@ +# Copyright (c) 2025, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestAPIRequestLog(UnitTestCase): + """ + Unit tests for APIRequestLog. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestAPIRequestLog(IntegrationTestCase): + """ + Integration tests for APIRequestLog. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index 09bf1fb2a6..157030b7fb 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -155,6 +155,7 @@ LOG_DOCTYPES = [ "Email Queue Recipient", "Error Log", "OAuth Bearer Token", + "API Request Log", ] diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 6c38d7f302..68640b518d 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -104,7 +104,9 @@ "allow_error_traceback", "enable_telemetry", "search_section", - "link_field_results_limit" + "link_field_results_limit", + "api_logging_section", + "log_api_requests" ], "fields": [ { @@ -707,12 +709,23 @@ "fieldname": "max_report_rows", "fieldtype": "Int", "label": "Max Report Rows" + }, + { + "fieldname": "api_logging_section", + "fieldtype": "Section Break", + "label": "API Logging" + }, + { + "default": "0", + "fieldname": "log_api_requests", + "fieldtype": "Check", + "label": "Log API Requests" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2025-05-19 14:17:40.748786", + "modified": "2025-05-21 17:03:23.310169", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 5caf06536c..d03bfcfa3e 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -66,6 +66,7 @@ class SystemSettings(Document): language: DF.Link lifespan_qrcode_image: DF.Int link_field_results_limit: DF.Int + log_api_requests: DF.Check login_with_email_link: DF.Check login_with_email_link_expiry: DF.Int logout_on_password_reset: DF.Check diff --git a/frappe/hooks.py b/frappe/hooks.py index 09539b6e47..bf4d8ac744 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -569,6 +569,7 @@ default_log_clearing_doctypes = { "Activity Log": 90, "Route History": 90, "OAuth Bearer Token": 30, + "API Request Log": 90, } # These keys will not be erased when doing frappe.clear_cache()