diff --git a/api/apps/restful_apis/system_api.py b/api/apps/restful_apis/system_api.py index 467d9111d..bae1f0eee 100644 --- a/api/apps/restful_apis/system_api.py +++ b/api/apps/restful_apis/system_api.py @@ -14,18 +14,25 @@ # limitations under the License. # +import json +import logging +from datetime import datetime +from timeit import default_timer as timer + from quart import jsonify from api.apps import login_required, current_user from api.utils.api_utils import get_json_result, get_data_error_result, server_error_response, generate_confirmation_token -from api.utils.health_utils import run_health_checks +from api.utils.health_utils import run_health_checks, get_oceanbase_status from common.versions import get_ragflow_version -from datetime import datetime from common.time_utils import current_timestamp, datetime_format from api.db.db_models import APIToken from api.db.services.api_service import APITokenService +from api.db.services.knowledgebase_service import KnowledgebaseService from api.db.services.user_service import UserTenantService from common.log_utils import get_log_levels, set_log_level +from common import settings +from rag.utils.redis_conn import REDIS_CONN @manager.route("/system/ping", methods=["GET"]) # noqa: F821 async def ping(): @@ -53,6 +60,174 @@ def version(): """ return get_json_result(data=get_ragflow_version()) + +@manager.route("/system/status", methods=["GET"]) # noqa: F821 +@login_required +def status(): + """ + Get the system status. + --- + tags: + - System + security: + - ApiKeyAuth: [] + responses: + 200: + description: System is operational. + schema: + type: object + properties: + es: + type: object + description: Elasticsearch status. + storage: + type: object + description: Storage status. + database: + type: object + description: Database status. + 503: + description: Service unavailable. + schema: + type: object + properties: + error: + type: string + description: Error message. + """ + res = {} + st = timer() + try: + res["doc_engine"] = settings.docStoreConn.health() + res["doc_engine"]["elapsed"] = "{:.1f}".format((timer() - st) * 1000.0) + except Exception as e: + res["doc_engine"] = { + "type": "unknown", + "status": "red", + "elapsed": "{:.1f}".format((timer() - st) * 1000.0), + "error": str(e), + } + + st = timer() + try: + settings.STORAGE_IMPL.health() + res["storage"] = { + "storage": settings.STORAGE_IMPL_TYPE.lower(), + "status": "green", + "elapsed": "{:.1f}".format((timer() - st) * 1000.0), + } + except Exception as e: + res["storage"] = { + "storage": settings.STORAGE_IMPL_TYPE.lower(), + "status": "red", + "elapsed": "{:.1f}".format((timer() - st) * 1000.0), + "error": str(e), + } + + st = timer() + try: + KnowledgebaseService.get_by_id("x") + res["database"] = { + "database": settings.DATABASE_TYPE.lower(), + "status": "green", + "elapsed": "{:.1f}".format((timer() - st) * 1000.0), + } + except Exception as e: + res["database"] = { + "database": settings.DATABASE_TYPE.lower(), + "status": "red", + "elapsed": "{:.1f}".format((timer() - st) * 1000.0), + "error": str(e), + } + + st = timer() + try: + if not REDIS_CONN.health(): + raise Exception("Lost connection!") + res["redis"] = { + "status": "green", + "elapsed": "{:.1f}".format((timer() - st) * 1000.0), + } + except Exception as e: + res["redis"] = { + "status": "red", + "elapsed": "{:.1f}".format((timer() - st) * 1000.0), + "error": str(e), + } + + task_executor_heartbeats = {} + try: + task_executors = REDIS_CONN.smembers("TASKEXE") + now = datetime.now().timestamp() + for task_executor_id in task_executors: + heartbeats = REDIS_CONN.zrangebyscore(task_executor_id, now - 60 * 30, now) + heartbeats = [json.loads(heartbeat) for heartbeat in heartbeats] + task_executor_heartbeats[task_executor_id] = heartbeats + except Exception: + logging.exception("get task executor heartbeats failed!") + res["task_executor_heartbeats"] = task_executor_heartbeats + + return get_json_result(data=res) + + +@manager.route("/system/oceanbase/status", methods=["GET"]) # noqa: F821 +@login_required +def oceanbase_status(): + """ + Get OceanBase health status and performance metrics. + --- + tags: + - System + security: + - ApiKeyAuth: [] + responses: + 200: + description: OceanBase status retrieved successfully. + schema: + type: object + properties: + status: + type: string + description: Status (alive/timeout). + message: + type: object + description: Detailed status information including health and performance metrics. + """ + try: + status_info = get_oceanbase_status() + return get_json_result(data=status_info) + except Exception as e: + return get_json_result( + data={ + "status": "error", + "message": f"Failed to get OceanBase status: {str(e)}" + }, + code=500 + ) + + +@manager.route("/system/config", methods=["GET"]) # noqa: F821 +def get_config(): + """ + Get system configuration. + --- + tags: + - System + responses: + 200: + description: Return system configuration + schema: + type: object + properties: + registerEnable: + type: integer 0 means disabled, 1 means enabled + description: Whether user registration is enabled + """ + return get_json_result(data={ + "registerEnabled": settings.REGISTER_ENABLED, + "disablePasswordLogin": settings.DISABLE_PASSWORD_LOGIN, + }) + @manager.route("/system/healthz", methods=["GET"]) # noqa: F821 def healthz(): result, all_ok = run_health_checks() diff --git a/api/apps/system_app.py b/api/apps/system_app.py deleted file mode 100644 index 833a7819d..000000000 --- a/api/apps/system_app.py +++ /dev/null @@ -1,197 +0,0 @@ -# -# Copyright 2024 The InfiniFlow Authors. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License -# -import logging -from datetime import datetime -import json - -from api.apps import login_required - -from api.db.services.knowledgebase_service import KnowledgebaseService -from api.utils.api_utils import ( - get_json_result, -) - -from timeit import default_timer as timer - -from rag.utils.redis_conn import REDIS_CONN -from api.utils.health_utils import get_oceanbase_status -from common import settings - -@manager.route("/status", methods=["GET"]) # noqa: F821 -@login_required -def status(): - """ - Get the system status. - --- - tags: - - System - security: - - ApiKeyAuth: [] - responses: - 200: - description: System is operational. - schema: - type: object - properties: - es: - type: object - description: Elasticsearch status. - storage: - type: object - description: Storage status. - database: - type: object - description: Database status. - 503: - description: Service unavailable. - schema: - type: object - properties: - error: - type: string - description: Error message. - """ - res = {} - st = timer() - try: - res["doc_engine"] = settings.docStoreConn.health() - res["doc_engine"]["elapsed"] = "{:.1f}".format((timer() - st) * 1000.0) - except Exception as e: - res["doc_engine"] = { - "type": "unknown", - "status": "red", - "elapsed": "{:.1f}".format((timer() - st) * 1000.0), - "error": str(e), - } - - st = timer() - try: - settings.STORAGE_IMPL.health() - res["storage"] = { - "storage": settings.STORAGE_IMPL_TYPE.lower(), - "status": "green", - "elapsed": "{:.1f}".format((timer() - st) * 1000.0), - } - except Exception as e: - res["storage"] = { - "storage": settings.STORAGE_IMPL_TYPE.lower(), - "status": "red", - "elapsed": "{:.1f}".format((timer() - st) * 1000.0), - "error": str(e), - } - - st = timer() - try: - KnowledgebaseService.get_by_id("x") - res["database"] = { - "database": settings.DATABASE_TYPE.lower(), - "status": "green", - "elapsed": "{:.1f}".format((timer() - st) * 1000.0), - } - except Exception as e: - res["database"] = { - "database": settings.DATABASE_TYPE.lower(), - "status": "red", - "elapsed": "{:.1f}".format((timer() - st) * 1000.0), - "error": str(e), - } - - st = timer() - try: - if not REDIS_CONN.health(): - raise Exception("Lost connection!") - res["redis"] = { - "status": "green", - "elapsed": "{:.1f}".format((timer() - st) * 1000.0), - } - except Exception as e: - res["redis"] = { - "status": "red", - "elapsed": "{:.1f}".format((timer() - st) * 1000.0), - "error": str(e), - } - - task_executor_heartbeats = {} - try: - task_executors = REDIS_CONN.smembers("TASKEXE") - now = datetime.now().timestamp() - for task_executor_id in task_executors: - heartbeats = REDIS_CONN.zrangebyscore(task_executor_id, now - 60 * 30, now) - heartbeats = [json.loads(heartbeat) for heartbeat in heartbeats] - task_executor_heartbeats[task_executor_id] = heartbeats - except Exception: - logging.exception("get task executor heartbeats failed!") - res["task_executor_heartbeats"] = task_executor_heartbeats - - return get_json_result(data=res) - -@manager.route("/oceanbase/status", methods=["GET"]) # noqa: F821 -@login_required -def oceanbase_status(): - """ - Get OceanBase health status and performance metrics. - --- - tags: - - System - security: - - ApiKeyAuth: [] - responses: - 200: - description: OceanBase status retrieved successfully. - schema: - type: object - properties: - status: - type: string - description: Status (alive/timeout). - message: - type: object - description: Detailed status information including health and performance metrics. - """ - try: - status_info = get_oceanbase_status() - return get_json_result(data=status_info) - except Exception as e: - return get_json_result( - data={ - "status": "error", - "message": f"Failed to get OceanBase status: {str(e)}" - }, - code=500 - ) - - -@manager.route("/config", methods=["GET"]) # noqa: F821 -def get_config(): - """ - Get system configuration. - --- - tags: - - System - responses: - 200: - description: Return system configuration - schema: - type: object - properties: - registerEnable: - type: integer 0 means disabled, 1 means enabled - description: Whether user registration is enabled - """ - return get_json_result(data={ - "registerEnabled": settings.REGISTER_ENABLED, - "disablePasswordLogin": settings.DISABLE_PASSWORD_LOGIN, - }) diff --git a/test/testcases/test_web_api/test_common.py b/test/testcases/test_web_api/test_common.py index b2edcd917..ab5ce042d 100644 --- a/test/testcases/test_web_api/test_common.py +++ b/test/testcases/test_web_api/test_common.py @@ -90,7 +90,7 @@ def system_delete_token(auth, token, *, headers=HEADERS): def system_status(auth, params=None, *, headers=HEADERS): - res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/status", headers=headers, auth=auth, params=params) + res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_API_URL}/status", headers=headers, auth=auth, params=params) return res.json() @@ -100,7 +100,7 @@ def system_version(auth, params=None, *, headers=HEADERS): def system_config(auth=None, params=None, *, headers=HEADERS): - res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_APP_URL}/config", headers=headers, auth=auth, params=params) + res = requests.get(url=f"{HOST_ADDRESS}{SYSTEM_API_URL}/config", headers=headers, auth=auth, params=params) return res.json() diff --git a/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py b/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py index f3e52d89e..6a2559b15 100644 --- a/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py +++ b/test/testcases/test_web_api/test_system_app/test_system_routes_unit.py @@ -156,7 +156,7 @@ def _load_system_module(monkeypatch): quart_mod.jsonify = lambda payload: payload monkeypatch.setitem(sys.modules, "quart", quart_mod) - module_path = repo_root / "api" / "apps" / "system_app.py" + module_path = repo_root / "api" / "apps" / "restful_apis" / "system_api.py" spec = importlib.util.spec_from_file_location("test_system_routes_unit_module", module_path) module = importlib.util.module_from_spec(spec) module.manager = _DummyManager() diff --git a/test/testcases/utils/engine_utils.py b/test/testcases/utils/engine_utils.py index 8a54bed21..aa67a4510 100644 --- a/test/testcases/utils/engine_utils.py +++ b/test/testcases/utils/engine_utils.py @@ -20,7 +20,7 @@ _DOC_ENGINE_CACHE = None def get_doc_engine(rag=None) -> str: - """Return lower-cased doc_engine from env, or from /system/status if env is unset.""" + """Return lower-cased doc_engine from env, or from /api/v1/system/status if env is unset.""" global _DOC_ENGINE_CACHE env = (os.getenv("DOC_ENGINE") or "").strip().lower() if env: @@ -34,9 +34,9 @@ def get_doc_engine(rag=None) -> str: api_url = getattr(rag, "api_url", "") if "/api/" in api_url: base_url, version = api_url.rsplit("/api/", 1) - status_url = f"{base_url}/{version}/system/status" + status_url = f"{base_url}/api/{version}/system/status" else: - status_url = f"{api_url}/system/status" + status_url = f"{api_url}/api/v1/system/status" headers = getattr(rag, "authorization_header", {}) res = requests.get(status_url, headers=headers).json() engine = str(res.get("data", {}).get("doc_engine", {}).get("type", "")).lower() diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 691ae9e7b..d89712cdf 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -175,8 +175,8 @@ export default { getSystemTokenList: `${restAPIv1}/system/tokens`, createSystemToken: `${restAPIv1}/system/tokens`, removeSystemToken: `${restAPIv1}/system/tokens`, - getSystemConfig: `${webAPI}/system/config`, - setLangfuseConfig: `${restAPIv1}/langfuse/api-key`, + getSystemConfig: `${restAPIv1}/system/config`, + setLangfuseConfig: `${restAPIv1}/langfuse/api_key`, // flow listTemplates: `${webAPI}/canvas/templates`,