mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 08:25:57 +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:
@@ -29,11 +29,12 @@ from app.gateway.routers import (
|
|||||||
uploads,
|
uploads,
|
||||||
)
|
)
|
||||||
from deerflow.config import app_config as deerflow_app_config
|
from deerflow.config import app_config as deerflow_app_config
|
||||||
|
from deerflow.config.app_config import apply_logging_level
|
||||||
|
|
||||||
AppConfig = deerflow_app_config.AppConfig
|
AppConfig = deerflow_app_config.AppConfig
|
||||||
get_app_config = deerflow_app_config.get_app_config
|
get_app_config = deerflow_app_config.get_app_config
|
||||||
|
|
||||||
# Configure logging
|
# Default logging; lifespan overrides from config.yaml log_level.
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
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
|
# Load config and check necessary environment variables at startup
|
||||||
try:
|
try:
|
||||||
app.state.config = get_app_config()
|
app.state.config = get_app_config()
|
||||||
|
apply_logging_level(app.state.config.log_level)
|
||||||
logger.info("Configuration loaded successfully")
|
logger.info("Configuration loaded successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Failed to load configuration during gateway startup: {e}"
|
error_msg = f"Failed to load configuration during gateway startup: {e}"
|
||||||
|
|||||||
+16
-24
@@ -34,50 +34,42 @@ _LOG_FMT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|||||||
_LOG_DATEFMT = "%Y-%m-%d %H:%M:%S"
|
_LOG_DATEFMT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
def _logging_level_from_config(name: str) -> int:
|
def _setup_logging(log_level: int = logging.INFO) -> None:
|
||||||
"""Map ``config.yaml`` ``log_level`` string to a ``logging`` level constant."""
|
"""Route logs to ``debug.log`` using *log_level* for the initial root/file setup.
|
||||||
mapping = logging.getLevelNamesMapping()
|
|
||||||
return mapping.get((name or "info").strip().upper(), logging.INFO)
|
|
||||||
|
|
||||||
|
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:
|
Note: later config-driven logging adjustments may change named logger
|
||||||
"""Send application logs to ``debug.log`` at *log_level*; do not print them on the console.
|
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
|
||||||
Idempotent: any pre-existing handlers on the root logger (e.g. installed by
|
this function's ``log_level`` argument.
|
||||||
``logging.basicConfig`` in transitively imported modules) are removed so the
|
|
||||||
debug session output only lands in ``debug.log``.
|
|
||||||
"""
|
"""
|
||||||
level = _logging_level_from_config(log_level)
|
|
||||||
root = logging.root
|
root = logging.root
|
||||||
for h in list(root.handlers):
|
for h in list(root.handlers):
|
||||||
root.removeHandler(h)
|
root.removeHandler(h)
|
||||||
h.close()
|
h.close()
|
||||||
root.setLevel(level)
|
root.setLevel(log_level)
|
||||||
|
|
||||||
file_handler = logging.FileHandler("debug.log", mode="a", encoding="utf-8")
|
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))
|
file_handler.setFormatter(logging.Formatter(_LOG_FMT, datefmt=_LOG_DATEFMT))
|
||||||
root.addHandler(file_handler)
|
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():
|
async def main():
|
||||||
# Install file logging first so warnings emitted while loading config do not
|
# Install file logging first so warnings emitted while loading config do not
|
||||||
# leak onto the interactive terminal via Python's lastResort handler.
|
# 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 import get_app_config
|
||||||
|
from deerflow.config.app_config import apply_logging_level
|
||||||
|
|
||||||
app_config = get_app_config()
|
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
|
# 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
|
# 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")
|
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):
|
class AppConfig(BaseModel):
|
||||||
"""Config for the DeerFlow application"""
|
"""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")
|
token_usage: TokenUsageConfig = Field(default_factory=TokenUsageConfig, description="Token usage tracking configuration")
|
||||||
models: list[ModelConfig] = Field(default_factory=list, description="Available models")
|
models: list[ModelConfig] = Field(default_factory=list, description="Available models")
|
||||||
sandbox: SandboxConfig = Field(description="Sandbox configuration")
|
sandbox: SandboxConfig = Field(description="Sandbox configuration")
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user