From 0d85a8e7aade138344adba7968e68fc8d122381c Mon Sep 17 00:00:00 2001 From: Zhichang Yu Date: Mon, 30 Mar 2026 18:40:58 +0800 Subject: [PATCH] feat: add dynamic log level adjustment APIs (#13850) Add REST APIs to dynamically query and modify log levels at runtime for both Python (Flask) and Go servers. Changes: - common/log_utils.py: add set_log_level() and get_log_levels() functions - admin/server/routes.py: add GET/PUT /api/v1/admin/log_levels endpoints - api/apps/system_app.py: add GET/PUT /api/{version}/system/log_levels endpoints - internal/logger/logger.go: add GetLevel() and SetLevel() with atomic level support - internal/handler/system.go: add GetLogLevel, SetLogLevel, Health handlers - internal/router/router.go: route /health to systemHandler - internal/admin/handler.go: add GetLogLevel, SetLogLevel handlers - internal/admin/router.go: add /api/v1/admin/log_level routes ### What problem does this PR solve? _Briefly describe what this PR aims to solve. Include background context that will help reviewers understand the purpose of the PR._ ### Type of change - [x] New Feature (non-breaking change which adds functionality) Co-authored-by: Claude Opus 4.6 --- admin/server/routes.py | 37 ++++++++++++++++++++++++++ api/apps/system_app.py | 54 ++++++++++++++++++++++++++++++++++++++ common/log_utils.py | 22 ++++++++++++++-- internal/admin/handler.go | 28 ++++++++++++++++++++ internal/admin/router.go | 3 +++ internal/handler/system.go | 49 ++++++++++++++++++++++++++++++++++ internal/logger/logger.go | 46 +++++++++++++++++++++++++++++--- internal/router/router.go | 8 +++--- 8 files changed, 237 insertions(+), 10 deletions(-) diff --git a/admin/server/routes.py b/admin/server/routes.py index 5d54245833..658cec48c0 100644 --- a/admin/server/routes.py +++ b/admin/server/routes.py @@ -30,6 +30,7 @@ from roles import RoleMgr from api.common.exceptions import AdminException from common.versions import get_ragflow_version from api.utils.api_utils import generate_confirmation_token +from common.log_utils import get_log_levels, set_log_level admin_bp = Blueprint("admin", __name__, url_prefix="/api/v1/admin") @@ -652,3 +653,39 @@ def test_sandbox_connection(): return error_response(str(e), 400) except Exception as e: return error_response(str(e), 500) + + +@admin_bp.route("/log_levels", methods=["GET"]) +@login_required +@check_admin_auth +def get_logger_levels(): + """Get current log levels for all packages.""" + try: + res = get_log_levels() + return success_response(res, "Get log levels", 0) + except Exception as e: + return error_response(str(e), 500) + + +@admin_bp.route("/log_levels", methods=["PUT"]) +@login_required +@check_admin_auth +def set_logger_level(): + """Set log level for a package.""" + try: + data = request.get_json() + if not data or "pkg_name" not in data or "level" not in data: + return error_response("pkg_name and level are required", 400) + + pkg_name = data["pkg_name"] + level = data["level"] + if not isinstance(pkg_name, str) or not isinstance(level, str): + return error_response("pkg_name and level must be strings", 400) + + success = set_log_level(pkg_name, level) + if success: + return success_response({"pkg_name": pkg_name, "level": level}, "Log level updated successfully") + else: + return error_response(f"Invalid log level: {level}", 400) + except Exception as e: + return error_response(str(e), 500) diff --git a/api/apps/system_app.py b/api/apps/system_app.py index da634ee7f0..5cbda3afd8 100644 --- a/api/apps/system_app.py +++ b/api/apps/system_app.py @@ -31,6 +31,7 @@ from api.utils.api_utils import ( ) from common.versions import get_ragflow_version from common.time_utils import current_timestamp, datetime_format +from common.log_utils import get_log_levels, set_log_level from timeit import default_timer as timer from rag.utils.redis_conn import REDIS_CONN @@ -375,3 +376,56 @@ def get_config(): "registerEnabled": settings.REGISTER_ENABLED, "disablePasswordLogin": settings.DISABLE_PASSWORD_LOGIN, }) + + +@manager.route("/log_levels", methods=["GET"]) # noqa: F821 +@login_required +async def get_logger_levels(): + """ + Get current log levels for all packages. + --- + tags: + - System + responses: + 200: + description: Return current log levels + """ + return get_json_result(data=get_log_levels()) + + +@manager.route("/log_levels", methods=["PUT"]) # noqa: F821 +@login_required +async def set_logger_level(): + """ + Set log level for a package. + --- + tags: + - System + parameters: + - in: body + name: body + required: true + schema: + type: object + properties: + pkg_name: + type: string + description: Package name (e.g., "rag.utils.es_conn") + level: + type: string + description: Log level (DEBUG, INFO, WARNING, ERROR) + responses: + 200: + description: Log level updated successfully + """ + from quart import request + data = await request.get_json() + if not data or "pkg_name" not in data or "level" not in data: + return get_data_error_result(message="pkg_name and level are required") + pkg_name = data["pkg_name"] + level = data["level"] + success = set_log_level(pkg_name, level) + if success: + return get_json_result(data={"pkg_name": pkg_name, "level": level}) + else: + return get_data_error_result(message=f"Invalid log level: {level}") diff --git a/common/log_utils.py b/common/log_utils.py index 7a5335aeae..af6b20fb2a 100644 --- a/common/log_utils.py +++ b/common/log_utils.py @@ -21,9 +21,10 @@ from logging.handlers import RotatingFileHandler from common.file_utils import get_project_base_directory initialized_root_logger = False +pkg_levels = {} # module-level to allow runtime modification def init_root_logger(logfile_basename: str, log_format: str = "%(asctime)-15s %(levelname)-8s %(process)d %(message)s"): - global initialized_root_logger + global initialized_root_logger, pkg_levels if initialized_root_logger: return initialized_root_logger = True @@ -46,7 +47,6 @@ def init_root_logger(logfile_basename: str, log_format: str = "%(asctime)-15s %( logging.captureWarnings(True) LOG_LEVELS = os.environ.get("LOG_LEVELS", "") - pkg_levels = {} for pkg_name_level in LOG_LEVELS.split(","): terms = pkg_name_level.split("=") if len(terms)!= 2: @@ -72,6 +72,24 @@ def init_root_logger(logfile_basename: str, log_format: str = "%(asctime)-15s %( logger.info(msg) +def set_log_level(pkg_name: str, level: str) -> bool: + """Set log level for a package at runtime. Returns True if successful.""" + global pkg_levels + level_value = logging.getLevelName(level.strip().upper()) + if not isinstance(level_value, int): + return False + pkg_levels[pkg_name] = logging.getLevelName(level_value) + pkg_logger = logging.getLogger(pkg_name) + pkg_logger.setLevel(level_value) + return True + + +def get_log_levels() -> dict: + """Get current log levels for all packages.""" + global pkg_levels + return dict(pkg_levels) + + def log_exception(e, *args): logging.exception(e) for a in args: diff --git a/internal/admin/handler.go b/internal/admin/handler.go index c90e9ea6d6..602092c5ce 100644 --- a/internal/admin/handler.go +++ b/internal/admin/handler.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "ragflow/internal/common" + "ragflow/internal/logger" "ragflow/internal/server" "ragflow/internal/service" "ragflow/internal/utility" @@ -1106,6 +1107,33 @@ func (h *Handler) HandleNoRoute(c *gin.Context) { }) } +// GetLogLevel returns the current log level +func (h *Handler) GetLogLevel(c *gin.Context) { + level := logger.GetLevel() + success(c, gin.H{"level": level}, "") +} + +// SetLogLevelRequest set log level request +type SetLogLevelRequest struct { + Level string `json:"level" binding:"required"` +} + +// SetLogLevel sets the log level at runtime +func (h *Handler) SetLogLevel(c *gin.Context) { + var req SetLogLevelRequest + if err := c.ShouldBindJSON(&req); err != nil { + errorResponse(c, "level is required", 400) + return + } + + if err := logger.SetLevel(req.Level); err != nil { + errorResponse(c, err.Error(), 400) + return + } + + success(c, gin.H{"level": req.Level}, "Log level updated successfully") +} + // Reports handle heartbeat reports from servers func (h *Handler) Reports(c *gin.Context) { var req common.BaseMessage diff --git a/internal/admin/router.go b/internal/admin/router.go index 377999a8b5..6895b07fbc 100644 --- a/internal/admin/router.go +++ b/internal/admin/router.go @@ -133,6 +133,9 @@ func (r *Router) Setup(engine *gin.Engine) { protected.POST("/license", r.handler.SetLicense) protected.POST("/license/config", r.handler.UpdateLicenseConfig) protected.GET("/license", r.handler.ShowLicense) + // Log level + protected.GET("/log_level", r.handler.GetLogLevel) + protected.PUT("/log_level", r.handler.SetLogLevel) } } diff --git a/internal/handler/system.go b/internal/handler/system.go index 781634f216..c2173ff8a2 100644 --- a/internal/handler/system.go +++ b/internal/handler/system.go @@ -18,6 +18,7 @@ package handler import ( "net/http" + "ragflow/internal/logger" "ragflow/internal/server" "ragflow/internal/service" @@ -47,6 +48,13 @@ func (h *SystemHandler) Ping(c *gin.Context) { c.String(http.StatusOK, "pong") } +// Health health check +func (h *SystemHandler) Health(c *gin.Context) { + c.JSON(200, gin.H{ + "status": "ok", + }) +} + // GetConfig get system configuration // @Summary Get System Configuration // @Description Get system configuration including register enabled status @@ -122,3 +130,44 @@ func (h *SystemHandler) GetVersion(c *gin.Context) { "data": version.Version, }) } + +// GetLogLevel returns the current log level +func (h *SystemHandler) GetLogLevel(c *gin.Context) { + level := logger.GetLevel() + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": gin.H{"level": level}, + }) +} + +// SetLogLevelRequest set log level request +type SetLogLevelRequest struct { + Level string `json:"level" binding:"required"` +} + +// SetLogLevel sets the log level at runtime +func (h *SystemHandler) SetLogLevel(c *gin.Context) { + var req SetLogLevelRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": "level is required", + }) + return + } + + if err := logger.SetLevel(req.Level); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 400, + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "Log level updated successfully", + "data": gin.H{"level": req.Level}, + }) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index d45313d37e..65ac2c7f20 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -19,14 +19,17 @@ package logger import ( "fmt" "runtime" + "sync" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) var ( - Logger *zap.Logger - Sugar *zap.SugaredLogger + Logger *zap.Logger + Sugar *zap.SugaredLogger + levelMu sync.RWMutex + atomicLevel zap.AtomicLevel ) // Init initializes the global logger @@ -47,6 +50,9 @@ func Init(level string) error { zapLevel = zapcore.InfoLevel } + // Create atomic level for dynamic updates + atomicLevel = zap.NewAtomicLevelAt(zapLevel) + // Custom encoder config to control output format encoderConfig := zapcore.EncoderConfig{ TimeKey: "timestamp", @@ -65,7 +71,7 @@ func Init(level string) error { // Configure zap config := zap.Config{ - Level: zap.NewAtomicLevelAt(zapLevel), + Level: atomicLevel, Development: false, Encoding: "console", EncoderConfig: encoderConfig, @@ -136,3 +142,37 @@ func Warn(msg string, fields ...zap.Field) { } Logger.Warn(msg, fields...) } + +// GetLevel returns the current log level +func GetLevel() string { + levelMu.RLock() + defer levelMu.RUnlock() + return atomicLevel.String() +} + +// SetLevel sets the log level at runtime +func SetLevel(level string) error { + levelMu.Lock() + defer levelMu.Unlock() + + var zapLevel zapcore.Level + switch level { + case "debug": + zapLevel = zapcore.DebugLevel + case "info": + zapLevel = zapcore.InfoLevel + case "warn", "warning": + zapLevel = zapcore.WarnLevel + case "error": + zapLevel = zapcore.ErrorLevel + case "fatal": + zapLevel = zapcore.FatalLevel + case "panic": + zapLevel = zapcore.PanicLevel + default: + return fmt.Errorf("unknown log level: %s", level) + } + + atomicLevel.SetLevel(zapLevel) + return nil +} diff --git a/internal/router/router.go b/internal/router/router.go index 95ef875818..18767ad7ba 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -81,17 +81,15 @@ func NewRouter( // Setup setup routes func (r *Router) Setup(engine *gin.Engine) { // Health check - engine.GET("/health", func(c *gin.Context) { - c.JSON(200, gin.H{ - "status": "ok", - }) - }) + engine.GET("/health", r.systemHandler.Health) // System endpoints engine.GET("/v1/system/ping", r.systemHandler.Ping) engine.GET("/v1/system/config", r.systemHandler.GetConfig) engine.GET("/v1/system/configs", r.systemHandler.GetConfigs) engine.GET("/v1/system/version", r.systemHandler.GetVersion) + engine.GET("/v1/system/log_level", r.systemHandler.GetLogLevel) + engine.PUT("/v1/system/log_level", r.systemHandler.SetLogLevel) engine.POST("/v1/user/register", r.userHandler.Register) // User login channels endpoint engine.GET("/v1/user/login/channels", r.userHandler.GetLoginChannels)