mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-11 22:25:41 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user