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 <noreply@anthropic.com>
This commit is contained in:
Zhichang Yu
2026-03-30 18:40:58 +08:00
committed by GitHub
parent 534729546e
commit 0d85a8e7aa
8 changed files with 237 additions and 10 deletions

View File

@@ -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)

View File

@@ -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}")

View File

@@ -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:

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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},
})
}

View File

@@ -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
}

View File

@@ -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)