mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-01 14:28:28 +00:00
fix(config): unify log_level from config.yaml across Gateway and debug entry points (#2601)
Centralize log level parsing in `logging_level_from_config()` and application in `apply_logging_level()` within `deerflow.config.app_config`. - Gateway lifespan applies configured log level on startup - `debug.py` uses shared helpers instead of local duplicates - `apply_logging_level()` targets only `deerflow`/`app` logger hierarchies so third-party library verbosity is not affected; root handler levels are only lowered (never raised) to allow configured loggers through without suppressing third-party output; root logger level is not modified - Config field description updated to clarify scope - Tests save/restore global logging state to avoid test pollution Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c0da278269
commit
eba3b9e18d
@ -29,11 +29,12 @@ from app.gateway.routers import (
|
||||
uploads,
|
||||
)
|
||||
from deerflow.config import app_config as deerflow_app_config
|
||||
from deerflow.config.app_config import apply_logging_level
|
||||
|
||||
AppConfig = deerflow_app_config.AppConfig
|
||||
get_app_config = deerflow_app_config.get_app_config
|
||||
|
||||
# Configure logging
|
||||
# Default logging; lifespan overrides from config.yaml log_level.
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
@ -164,6 +165,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
# Load config and check necessary environment variables at startup
|
||||
try:
|
||||
app.state.config = get_app_config()
|
||||
apply_logging_level(app.state.config.log_level)
|
||||
logger.info("Configuration loaded successfully")
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to load configuration during gateway startup: {e}"
|
||||
|
||||
@ -34,50 +34,42 @@ _LOG_FMT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
_LOG_DATEFMT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
def _logging_level_from_config(name: str) -> int:
|
||||
"""Map ``config.yaml`` ``log_level`` string to a ``logging`` level constant."""
|
||||
mapping = logging.getLevelNamesMapping()
|
||||
return mapping.get((name or "info").strip().upper(), logging.INFO)
|
||||
def _setup_logging(log_level: int = logging.INFO) -> None:
|
||||
"""Route logs to ``debug.log`` using *log_level* for the initial root/file setup.
|
||||
|
||||
This configures the root logger and the ``debug.log`` file handler so logs do
|
||||
not print on the interactive console. It is idempotent: any pre-existing
|
||||
handlers on the root logger (e.g. installed by ``logging.basicConfig`` in
|
||||
transitively imported modules) are removed so the debug session output only
|
||||
lands in ``debug.log``.
|
||||
|
||||
def _setup_logging(log_level: str) -> None:
|
||||
"""Send application logs to ``debug.log`` at *log_level*; do not print them on the console.
|
||||
|
||||
Idempotent: any pre-existing handlers on the root logger (e.g. installed by
|
||||
``logging.basicConfig`` in transitively imported modules) are removed so the
|
||||
debug session output only lands in ``debug.log``.
|
||||
Note: later config-driven logging adjustments may change named logger
|
||||
verbosity without raising the root logger or file-handler thresholds set
|
||||
here, so the eventual contents of ``debug.log`` may not be filtered solely by
|
||||
this function's ``log_level`` argument.
|
||||
"""
|
||||
level = _logging_level_from_config(log_level)
|
||||
root = logging.root
|
||||
for h in list(root.handlers):
|
||||
root.removeHandler(h)
|
||||
h.close()
|
||||
root.setLevel(level)
|
||||
root.setLevel(log_level)
|
||||
|
||||
file_handler = logging.FileHandler("debug.log", mode="a", encoding="utf-8")
|
||||
file_handler.setLevel(level)
|
||||
file_handler.setLevel(log_level)
|
||||
file_handler.setFormatter(logging.Formatter(_LOG_FMT, datefmt=_LOG_DATEFMT))
|
||||
root.addHandler(file_handler)
|
||||
|
||||
|
||||
def _update_logging_level(log_level: str) -> None:
|
||||
"""Update the root logger and existing handlers to *log_level*."""
|
||||
level = _logging_level_from_config(log_level)
|
||||
root = logging.root
|
||||
root.setLevel(level)
|
||||
for handler in root.handlers:
|
||||
handler.setLevel(level)
|
||||
|
||||
|
||||
async def main():
|
||||
# Install file logging first so warnings emitted while loading config do not
|
||||
# leak onto the interactive terminal via Python's lastResort handler.
|
||||
_setup_logging("info")
|
||||
_setup_logging()
|
||||
|
||||
from deerflow.config import get_app_config
|
||||
from deerflow.config.app_config import apply_logging_level
|
||||
|
||||
app_config = get_app_config()
|
||||
_update_logging_level(app_config.log_level)
|
||||
apply_logging_level(app_config.log_level)
|
||||
|
||||
# Delay the rest of the deerflow imports until *after* logging is installed
|
||||
# so that any import-time side effects (e.g. deerflow.agents starts a
|
||||
|
||||
@ -53,10 +53,34 @@ def _default_config_candidates() -> tuple[Path, ...]:
|
||||
return (backend_dir / "config.yaml", repo_root / "config.yaml")
|
||||
|
||||
|
||||
def logging_level_from_config(name: str | None) -> int:
|
||||
"""Map ``config.yaml`` ``log_level`` string to a :mod:`logging` level constant."""
|
||||
mapping = logging.getLevelNamesMapping()
|
||||
return mapping.get((name or "info").strip().upper(), logging.INFO)
|
||||
|
||||
|
||||
def apply_logging_level(name: str | None) -> None:
|
||||
"""Resolve *name* to a logging level and apply it to the ``deerflow``/``app`` logger hierarchies.
|
||||
|
||||
Only the ``deerflow`` and ``app`` logger levels are changed so that
|
||||
third-party library verbosity (e.g. uvicorn, sqlalchemy) is not
|
||||
affected. Root handler levels are lowered (never raised) so that
|
||||
messages from the configured loggers can propagate through without
|
||||
being filtered, while preserving handler thresholds that may be
|
||||
intentionally restrictive for third-party log output.
|
||||
"""
|
||||
level = logging_level_from_config(name)
|
||||
for logger_name in ("deerflow", "app"):
|
||||
logging.getLogger(logger_name).setLevel(level)
|
||||
for handler in logging.root.handlers:
|
||||
if level < handler.level:
|
||||
handler.setLevel(level)
|
||||
|
||||
|
||||
class AppConfig(BaseModel):
|
||||
"""Config for the DeerFlow application"""
|
||||
|
||||
log_level: str = Field(default="info", description="Logging level for deerflow modules (debug/info/warning/error)")
|
||||
log_level: str = Field(default="info", description="Logging level for deerflow and app modules (debug/info/warning/error); third-party libraries are not affected")
|
||||
token_usage: TokenUsageConfig = Field(default_factory=TokenUsageConfig, description="Token usage tracking configuration")
|
||||
models: list[ModelConfig] = Field(default_factory=list, description="Available models")
|
||||
sandbox: SandboxConfig = Field(description="Sandbox configuration")
|
||||
|
||||
91
backend/tests/test_logging_level_from_config.py
Normal file
91
backend/tests/test_logging_level_from_config.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""Tests for ``logging_level_from_config`` and ``apply_logging_level`` (``config.yaml`` ``log_level`` mapping)."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from deerflow.config.app_config import apply_logging_level, logging_level_from_config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "expected"),
|
||||
[
|
||||
("debug", logging.DEBUG),
|
||||
("INFO", logging.INFO),
|
||||
("warning", logging.WARNING),
|
||||
("error", logging.ERROR),
|
||||
("critical", logging.CRITICAL),
|
||||
(" Debug ", logging.DEBUG),
|
||||
(None, logging.INFO),
|
||||
("", logging.INFO),
|
||||
],
|
||||
)
|
||||
def test_logging_level_from_config_known_and_defaults(name: str | None, expected: int) -> None:
|
||||
assert logging_level_from_config(name) == expected
|
||||
|
||||
|
||||
def test_logging_level_from_config_unknown_falls_back_to_info() -> None:
|
||||
assert logging_level_from_config("not-a-real-level-name") == logging.INFO
|
||||
|
||||
|
||||
class TestApplyLoggingLevel:
|
||||
"""Tests for ``apply_logging_level`` — verifies deerflow/app logger and handler levels."""
|
||||
|
||||
def setup_method(self) -> None:
|
||||
root = logging.root
|
||||
self._original_root_level = root.level
|
||||
self._original_root_handlers = list(root.handlers)
|
||||
self._original_handler_levels = {handler: handler.level for handler in self._original_root_handlers}
|
||||
self._original_deerflow_level = logging.getLogger("deerflow").level
|
||||
self._original_app_level = logging.getLogger("app").level
|
||||
|
||||
def teardown_method(self) -> None:
|
||||
root = logging.root
|
||||
current_handlers = list(root.handlers)
|
||||
|
||||
for handler in current_handlers:
|
||||
if handler not in self._original_root_handlers:
|
||||
root.removeHandler(handler)
|
||||
handler.close()
|
||||
|
||||
for handler in list(root.handlers):
|
||||
root.removeHandler(handler)
|
||||
|
||||
for handler in self._original_root_handlers:
|
||||
handler.setLevel(self._original_handler_levels[handler])
|
||||
root.addHandler(handler)
|
||||
|
||||
root.setLevel(self._original_root_level)
|
||||
logging.getLogger("deerflow").setLevel(self._original_deerflow_level)
|
||||
logging.getLogger("app").setLevel(self._original_app_level)
|
||||
|
||||
def test_sets_deerflow_app_logger_levels(self) -> None:
|
||||
apply_logging_level("debug")
|
||||
assert logging.getLogger("deerflow").level == logging.DEBUG
|
||||
assert logging.getLogger("app").level == logging.DEBUG
|
||||
|
||||
def test_lowers_handler_level(self) -> None:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(logging.WARNING)
|
||||
logging.root.addHandler(handler)
|
||||
apply_logging_level("debug")
|
||||
assert handler.level == logging.DEBUG
|
||||
|
||||
def test_does_not_raise_handler_level(self) -> None:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setLevel(logging.WARNING)
|
||||
logging.root.addHandler(handler)
|
||||
apply_logging_level("error")
|
||||
assert handler.level == logging.WARNING
|
||||
|
||||
def test_does_not_modify_root_logger(self) -> None:
|
||||
logging.root.setLevel(logging.WARNING)
|
||||
apply_logging_level("debug")
|
||||
assert logging.root.level == logging.WARNING
|
||||
apply_logging_level("error")
|
||||
assert logging.root.level == logging.WARNING
|
||||
|
||||
def test_defaults_to_info(self) -> None:
|
||||
apply_logging_level(None)
|
||||
assert logging.getLogger("deerflow").level == logging.INFO
|
||||
assert logging.getLogger("app").level == logging.INFO
|
||||
Loading…
x
Reference in New Issue
Block a user