Compare commits

...

14 Commits

Author SHA1 Message Date
Yuge Zhang 6f174aa07e . 2025-11-13 22:06:43 +08:00
Yuge Zhang cc3351b0ee fix 2025-11-13 21:43:06 +08:00
Yuge Zhang 3c59a8c04e Merge branch 'main' of github.com:microsoft/agent-lightning into feature/logging 2025-11-13 21:35:25 +08:00
Yuge Zhang 423837924a . 2025-11-13 20:18:29 +08:00
Yuge Zhang 987e6a716b fix lint 2025-11-13 20:18:21 +08:00
Yuge Zhang 6c478aeea0 . 2025-11-13 20:04:50 +08:00
Yuge Zhang fb33cc7e71 docs 2025-11-13 19:55:00 +08:00
Yuge Zhang 9be342f3bf . 2025-11-13 19:50:02 +08:00
Yuge Zhang df7c66471d . 2025-11-13 17:47:12 +08:00
Yuge Zhang a9cfbd4658 use setup logging in favor of configure_logger 2025-11-13 17:32:36 +08:00
Yuge Zhang 1b072d28f3 . 2025-11-13 17:31:10 +08:00
Yuge Zhang 3f38f6b371 update tests 2025-11-13 17:27:16 +08:00
Yuge Zhang dacf142f1b add unit tests 2025-11-13 16:27:57 +08:00
Yuge Zhang c4f4267fba new logging 2025-11-13 15:58:18 +08:00
26 changed files with 802 additions and 56 deletions
+3 -1
View File
@@ -10,7 +10,9 @@ from .emitter import *
from .execution import *
from .litagent import *
from .llm_proxy import *
from .logging import *
from .logging import configure_logger # deprecated # type: ignore
from .logging import setup as setup_logging # type: ignore
from .logging import setup_module as setup_module_logging # type: ignore
from .runner import *
from .server import AgentLightningServer # deprecated # type: ignore
from .store import *
+2 -2
View File
@@ -9,7 +9,7 @@ import asyncio
import logging
from typing import Iterable
from agentlightning.logging import configure_logger
from agentlightning import setup_logging
from agentlightning.store.client_server import LightningStoreServer
from agentlightning.store.memory import InMemoryLightningStore
@@ -27,7 +27,7 @@ def main(argv: Iterable[str] | None = None) -> int:
)
args = parser.parse_args(list(argv) if argv is not None else None)
configure_logger()
setup_logging()
store = InMemoryLightningStore()
server = LightningStoreServer(
+329 -13
View File
@@ -1,10 +1,18 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
import os
import platform
import sys
import warnings
from logging.config import dictConfig
from typing import Any, Dict, Optional
__all__ = ["configure_logger"]
from rich.console import Console
__all__ = ["setup", "configure_logger", "setup_module"]
def configure_logger(level: int = logging.INFO, name: str = "agentlightning") -> logging.Logger:
@@ -15,6 +23,10 @@ def configure_logger(level: int = logging.INFO, name: str = "agentlightning") ->
not propagate to the root logger, preventing duplicate log emission when
applications compose multiple logging configurations.
!!! danger
This function is deprecated in favor of [`setup_logging`][agentlightning.setup_logging].
Args:
level: Logging level applied both to the logger and the installed
handler. Defaults to `logging.INFO`.
@@ -32,23 +44,327 @@ def configure_logger(level: int = logging.INFO, name: str = "agentlightning") ->
logger.info("agent-lightning is ready!")
```
"""
warnings.warn("This function is deprecated in favor of `setup_logging`.", DeprecationWarning, stacklevel=2)
return setup_module(level=level, name=name, console=True, color=True, propagate=False)
DEFAULT_FORMAT = "%(asctime)s [%(levelname)s] (Process-%(process)d %(name)s) %(message)s"
DATE_FORMAT = "%H:%M:%S"
def _to_level_value(lvl: int | str) -> int:
if isinstance(lvl, int):
return lvl
val = getattr(logging, str(lvl).upper(), None)
if val is None:
raise ValueError(f"Invalid log level: {lvl}")
return val
def _ensure_file_handler(
logger: logging.Logger,
filename: str,
*,
level: int,
formatter: Optional[logging.Formatter],
) -> None:
"""Attach a FileHandler to `logger` for `filename` if it doesn't already exist."""
abspath = os.path.abspath(filename)
# Avoid duplicates
for h in logger.handlers:
if isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", None) == abspath:
return
# Ensure directory exists
dirname = os.path.dirname(abspath)
if dirname:
os.makedirs(dirname, exist_ok=True)
fh = logging.FileHandler(abspath, encoding="utf-8")
fh.setLevel(level)
if formatter is not None:
fh.setFormatter(formatter)
else:
fh.setFormatter(logging.Formatter(DEFAULT_FORMAT, DATE_FORMAT))
logger.addHandler(fh)
def setup(
level: int | str = "INFO",
*,
console: bool = True,
color: bool | Dict[str, Any] = True,
propagate: bool = False,
disable_existing_loggers: bool = False,
capture_warnings: bool = False,
submodule_levels: Optional[dict[str, int | str]] = None,
extra_handlers: Optional[list[logging.Handler]] = None,
formatter: Optional[logging.Formatter] = None,
apply_to: Optional[list[str]] = None,
files: Optional[str | dict[str, str]] = None,
) -> None:
"""Configures logging for the `agentlightning` logger hierarchy.
This function provides a one-stop setup utility for configuring the
`agentlightning` root logger and optionally its submodules or external
loggers. It supports console logging, colored rich output, per-submodule
log levels, and optional handler/formatter injection.
The setup is intentionally isolated: it does not modify the global root
logger or loggers belonging to other libraries unless explicitly directed
via `apply_to`.
Args:
level:
Logging level for the base `agentlightning` logger. Accepts either
an integer (e.g., `logging.DEBUG`) or a string level name
(e.g., `"INFO"`). Defaults to `"INFO"`.
console:
Whether to attach a console handler to the logger. Defaults to
`True`.
color:
Enables rich-formatted output using `RichHandler` when `True`
or a configuration dict. If `False`, a plain text formatter is
used instead. Defaults to `True`.
propagate:
Whether `agentlightning` logs should propagate to ancestor
loggers. Defaults to `False`.
disable_existing_loggers:
Passed to `logging.config.dictConfig`. If `True`, disables all
existing configured loggers before applying this configuration.
Defaults to `False`.
capture_warnings:
If `True`, redirects Python `warnings` emitted via the `warnings`
module into the logging system. Defaults to `False`.
submodule_levels:
Mapping of submodule logger names to logging levels. If a specified
submodule level is more verbose than the base level, a warning is emitted.
extra_handlers:
A list of user-provided handlers to attach to the `agentlightning` logger.
Handlers are added idempotently; duplicates are not reattached.
formatter:
A formatter to apply to any handler under `agentlightning` that does not
already have one assigned. Useful for customizing output without overwriting
formatters on custom handlers.
apply_to:
A list of additional logger names to configure identically to
`agentlightning` base logger. Their handlers are replaced with copies of the base
handlers, and propagation is disabled to avoid duplicate log emission.
files:
If a string, attach a FileHandler to the base `agentlightning` logger.
If a dict, for each `(logger_name, filename)` pair, attach a FileHandler
directly to that logger.
Each file handler should use the logger's effective level at creation.
Notes:
* On Windows, this function forces UTF-8 mode in the console to prevent
issues with rich output or special characters.
* Submodule loggers can generate records below the handler's emission
threshold. Whether such records appear depends on both the logger's
level and the handler's level.
* `apply_to` loggers inherit the same handlers but do not propagate
upward, yielding isolated, consistent behavior.
Examples:
Basic setup:
>>> setup()
Enabling debug mode with no color:
>>> setup(level="DEBUG", color=False)
Overriding specific submodule levels:
>>> setup(submodule_levels={"agentlightning.io": "DEBUG"})
Attaching an additional file handler:
>>> fh = logging.FileHandler("app.log")
>>> setup(extra_handlers=[fh])
"""
# Ensure UTF-8 encoding on Windows consoles
# Note: This change does not fully represent support for execution under the windown system.
# Note: This change does not fully represent support for execution under the windows system.
# It only fixes console printing issues caused by special characters.
# TODO: More comprehensive Windows support may be needed in the future.
if platform.system() == "Windows":
os.environ["PYTHONUTF8"] = "1"
logger = logging.getLogger(name)
logger.handlers.clear() # clear existing handlers
base_logger = setup_module(
level,
name="agentlightning",
console=console,
color=color,
propagate=propagate,
disable_existing_loggers=disable_existing_loggers,
)
# log to stdout
handler = logging.StreamHandler()
handler.setLevel(level)
formatter = logging.Formatter("%(asctime)s [%(levelname)s] (Process-%(process)d %(name)s) %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(level)
logger.propagate = False # prevent double logging
return logger
base_level_value = base_logger.level
# Apply user-provided formatter (only to handlers without one,
# so we don't clobber custom extra_handlers)
if formatter is not None:
for h in base_logger.handlers:
if h.formatter is None:
h.setFormatter(formatter)
# Attach user-provided handler(s) if any, idempotently
if extra_handlers:
for h in extra_handlers:
if h not in base_logger.handlers:
base_logger.addHandler(h)
# Per-submodule levels
if submodule_levels:
for name, lvl in submodule_levels.items():
sub_level = _to_level_value(lvl)
# Emit a warning if submodule level is lower (more verbose) than the global/base level
if sub_level < base_level_value:
base_logger.warning(
"Submodule logger '%s' level %s (%s) is more verbose than base "
"logger level %s (%s). Records below the base level may still be "
"filtered out by handlers depending on their own levels.",
name,
lvl,
sub_level,
logging.getLevelName(base_level_value),
base_level_value,
)
# The logger will *create* records down to the logger's level, but a handler
# with a higher level will still drop anything below its own threshold.
# Effective emission is gated by both: record.level >= logger.level AND handler.level.
logging.getLogger(name).setLevel(lvl)
# Attach file handlers if requested
if files is not None:
if isinstance(files, str):
# Single file for the entire `agentlightning` hierarchy.
_ensure_file_handler(
logger=base_logger,
filename=files,
level=base_level_value,
formatter=formatter,
)
else:
# Per-logger files
for logger_name, filename in files.items():
lg = logging.getLogger(logger_name)
# Use the logger's *effective* level at creation time
effective_level = lg.getEffectiveLevel()
_ensure_file_handler(
logger=lg,
filename=filename,
level=effective_level,
formatter=formatter,
)
# Optionally apply the same handler setup to other loggers outside this module
if apply_to:
for name in apply_to:
lg = logging.getLogger(name)
# This removes any existing handlers so we don't duplicate output
# and ensures these loggers share exactly the same handlers as base_logger.
lg.handlers.clear()
for h in base_logger.handlers:
lg.addHandler(h)
lg.setLevel(base_logger.level)
# We've attached handlers directly to these loggers; if propagate
# stayed True, records would bubble up to ancestor loggers and could be
# emitted twice (here and on the parent/root). Setting False isolates them.
lg.propagate = False
# Optionally capture warnings
if capture_warnings:
logging.captureWarnings(True)
def setup_module(
level: int | str = "INFO",
*,
name: str = "agentlightning",
console: bool = True,
color: bool | Dict[str, Any] = True,
propagate: bool = False,
disable_existing_loggers: bool = False,
) -> logging.Logger:
"""Initializes and returns the base logger for `agentlightning`.
This function constructs and applies a `dictConfig` configuration for the
logger hierarchy rooted at `name`. It supports either rich console
formatting (via `RichHandler`) or plain text formatting, based on the
`color` argument.
Unlike [`setup_logging`][agentlightning.setup_logging], this function configures only a single logger namespace
and does not attach extra handlers or submodule levels. It is primarily used
internally by [`setup_logging`][agentlightning.setup_logging] but is also suitable for direct integration in
custom logging workflows.
"""
root_cfg: Dict[str, Any] = {
"version": 1,
"disable_existing_loggers": disable_existing_loggers,
"loggers": {
name: {
"handlers": [],
"level": level,
"propagate": propagate,
}
},
"handlers": {},
"formatters": {},
}
# Choose formatter / handler definition
if color is not False and console:
# Console must be true to display colored outputs
if isinstance(color, dict):
rich_handler_config = color
else:
rich_handler_config: Dict[str, Any] = {
"rich_tracebacks": False,
"markup": False,
"show_time": True,
"show_path": True,
}
if not _has_width():
# e.g., in a CI environment.
rich_handler_config["console"] = Console(width=200)
root_cfg["handlers"]["console"] = {
"class": "rich.logging.RichHandler",
"level": level,
**rich_handler_config,
}
# RichHandler manages its own style; keep formatter None
else:
fmt_name = "plain"
root_cfg["formatters"][fmt_name] = {
"format": DEFAULT_FORMAT,
"datefmt": DATE_FORMAT,
}
if console:
root_cfg["handlers"]["console"] = {
"class": "logging.StreamHandler",
"level": level,
"formatter": fmt_name,
}
# Attach selected handlers to agentlightning
handler_names = list(root_cfg["handlers"].keys())
root_cfg["loggers"][name]["handlers"] = handler_names
# Apply dictConfig (this resets the logger handlers)
dictConfig(root_cfg)
return logging.getLogger(name)
def _has_width() -> bool:
"""Automatically determine whether the terminal has a width."""
return sys.stdout.isatty()
+4
View File
@@ -1067,6 +1067,8 @@ class LightningStoreClient(LightningStore):
"server_address": self.server_address,
"_retry_delays": self._retry_delays,
"_health_retry_delays": self._health_retry_delays,
"_request_timeout": self._request_timeout,
"_connection_timeout": self._connection_timeout,
}
def __setstate__(self, state: Dict[str, Any]):
@@ -1080,6 +1082,8 @@ class LightningStoreClient(LightningStore):
self._lock = threading.Lock()
self._retry_delays = state["_retry_delays"]
self._health_retry_delays = state["_health_retry_delays"]
self._request_timeout = state["_request_timeout"]
self._connection_timeout = state["_connection_timeout"]
self._dequeue_was_successful = False
self._dequeue_first_unsuccessful = True
+2 -2
View File
@@ -18,13 +18,13 @@ from flask import Flask, Response, abort, request
from tensordict import TensorDict
from verl import DataProto
from agentlightning import LLM, AgentLightningServer, NamedResources, RolloutLegacy, configure_logger
from agentlightning import LLM, AgentLightningServer, NamedResources, RolloutLegacy, setup_logging
from agentlightning.adapter.triplet import TracerTraceToTriplet, TraceToTripletBase
from agentlightning.llm_proxy import LLMProxy, ModelConfig
from agentlightning.store.base import LightningStore
from agentlightning.types import Rollout, RolloutConfig, Task
configure_logger()
setup_logging()
__all__ = [
"AgentModeDaemon",
+8
View File
@@ -23,3 +23,11 @@
## CLI Builder
::: agentlightning.lightning_cli
## Logging
::: agentlightning.configure_logger
::: agentlightning.setup_module_logging
::: agentlightning.setup_logging
+1 -1
View File
@@ -181,5 +181,5 @@ async def main():
if __name__ == "__main__":
agl.configure_logger()
agl.setup_logging()
asyncio.run(main())
+2 -2
View File
@@ -19,7 +19,7 @@ python apo_custom_algorithm.py runner
from apo_custom_algorithm import apo_algorithm, apo_rollout
from rich.console import Console
from agentlightning import Trainer, configure_logger
from agentlightning import Trainer, setup_logging
from agentlightning.algorithm import algo
from agentlightning.store import LightningStore
@@ -39,6 +39,6 @@ async def apo_algorithm_usable_in_trainer(*, store: LightningStore):
if __name__ == "__main__":
configure_logger()
setup_logging()
trainer = Trainer(n_workers=1, algorithm=apo_algorithm_usable_in_trainer)
trainer.fit(apo_rollout)
+2 -2
View File
@@ -8,7 +8,7 @@ from typing import cast
from apo_custom_algorithm import apo_rollout
from agentlightning import Trainer, configure_logger
from agentlightning import Trainer, setup_logging
from agentlightning.litagent import LitAgent
from agentlightning.runner import LitAgentRunner
from agentlightning.store import InMemoryLightningStore
@@ -105,7 +105,7 @@ def debug_with_trainer():
if __name__ == "__main__":
configure_logger()
setup_logging()
parser = argparse.ArgumentParser(description="Debug APO with runner or trainer approach.")
parser.add_argument(
+2 -2
View File
@@ -12,7 +12,7 @@ from typing import Any
import dotenv
from openai import OpenAI
from agentlightning import configure_logger
from agentlightning import setup_logging
from agentlightning.litagent import LitAgent
from agentlightning.trainer import Trainer
@@ -40,7 +40,7 @@ class SimpleAgent(LitAgent[Any]):
if __name__ == "__main__":
configure_logger()
setup_logging()
dotenv.load_dotenv()
agent = SimpleAgent()
# Use 2 workers to simulate multiple clients
+2 -2
View File
@@ -8,7 +8,7 @@ from typing import Tuple, cast
from openai import AsyncOpenAI
from room_selector import RoomSelectionTask, load_room_tasks, prompt_template_baseline, room_selector
from agentlightning import Trainer, configure_logger
from agentlightning import Trainer, setup_logging
from agentlightning.adapter import TraceToMessages
from agentlightning.algorithm.apo import APO
from agentlightning.types import Dataset
@@ -33,7 +33,7 @@ def setup_apo_logger(file_path: str = "apo.log") -> None:
def main() -> None:
configure_logger()
setup_logging()
setup_apo_logger()
openai_client = AsyncOpenAI()
+2 -2
View File
@@ -2,7 +2,7 @@
from aoai_finetune import AzureOpenAIFinetune
from agentlightning import configure_logger
from agentlightning import setup_logging
finetune_algo = AzureOpenAIFinetune(
base_deployment_name="gpt-4.1-mini",
@@ -12,7 +12,7 @@ finetune_algo = AzureOpenAIFinetune(
data_filter_ratio=0.6,
)
configure_logger()
setup_logging()
def test_deployment():
+2 -2
View File
@@ -5,13 +5,13 @@ from aoai_finetune import AzureOpenAIFinetune
from capital_agent import capital_agent
from rich.console import Console
from agentlightning import TraceToMessages, Trainer, configure_logger
from agentlightning import TraceToMessages, Trainer, setup_logging
console = Console()
def main():
configure_logger()
setup_logging()
finetune_algo = AzureOpenAIFinetune(
base_deployment_name="gpt-4.1-mini",
finetuned_deployment_name="gpt-4.1-mini-ft",
+2 -2
View File
@@ -19,9 +19,9 @@ from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_ext.tools.mcp import McpWorkbench, StdioServerParams
from eval_utils import evaluate_v0_1
from agentlightning import LLM, LitAgent, NamedResources, Trainer, configure_logger
from agentlightning import LLM, LitAgent, NamedResources, Trainer, setup_logging
configure_logger()
setup_logging()
calculator_mcp_server = StdioServerParams(command="uvx", args=["mcp-server-calculator"])
+2 -2
View File
@@ -15,10 +15,10 @@ from agentlightning import (
LitAgent,
NamedResources,
Trainer,
configure_logger,
setup_logging,
)
configure_logger()
setup_logging()
agent_prompt = """You are an assistant who answers questions using Wikipedia retriever. Answer the question using only the retrieved passages. Verify your answer directly against the text.
+2 -2
View File
@@ -8,9 +8,9 @@ import requests
from openai import OpenAI
from qa_em import compute_score_em
from agentlightning import LLM, LitAgent, NamedResources, Trainer, configure_logger, reward
from agentlightning import LLM, LitAgent, NamedResources, Trainer, reward, setup_logging
configure_logger()
setup_logging()
# Copied and adapted from https://github.com/PeterGriffinJin/Search-R1/blob/main/scripts/data_process/nq_search.py
INSTRUCTION_FORMAT = """Answer the given question. You must conduct reasoning inside <think> and </think> first every time you get new information. After reasoning, if you find you lack some knowledge, you can call a search engine by <search> query </search> and it will return the top searched results between <information> and </information>. You can search as many times as your want. If you find no further external knowledge needed, you can directly provide the answer inside <answer> and </answer>, without detailed illustrations. For example, <answer> Beijing </answer>. Question: """
+3 -2
View File
@@ -9,6 +9,7 @@ as well as https://langchain-ai.github.io/langgraph/tutorials/sql-agent/
from __future__ import annotations
import logging
import os
import re
import shutil
@@ -29,9 +30,9 @@ from spider_eval.exec_eval import eval_exec_match
import agentlightning as agl
agl.configure_logger()
agl.setup_logging(apply_to=[__name__])
logger = agl.configure_logger(name=__name__)
logger = logging.getLogger(__name__)
WRITE_QUERY_PROMPT = ChatPromptTemplate(
+1 -1
View File
@@ -192,7 +192,7 @@ def main():
args = parser.parse_args()
agl.configure_logger()
agl.setup_logging()
if args.mode == "algo":
run_algo()
elif args.mode == "runner":
+1 -1
View File
@@ -367,7 +367,7 @@ def main() -> None:
runner_parser.set_defaults(func=_run_runner)
args = parser.parse_args()
agl.configure_logger()
agl.setup_logging()
args.func(args)
+2 -4
View File
@@ -7,7 +7,6 @@ It should be included in CI in future if we decided to maintain this example.
"""
import asyncio
import logging
from typing import cast
import openai
@@ -24,13 +23,12 @@ from agentlightning import (
LLMProxy,
LlmProxyTraceToTriplet,
TracerTraceToTriplet,
configure_logger,
emit_reward,
setup_logging,
)
from agentlightning.store import LightningStoreThreaded
configure_logger(name="agentlightning")
configure_logger(name="agl_tinker", level=logging.INFO)
setup_logging(apply_to=["agl_tinker"])
async def test_tracer():
+2 -2
View File
@@ -29,7 +29,7 @@ from openai import AsyncOpenAI
from rich.console import Console
from trl import SFTConfig, SFTTrainer # type: ignore
from agentlightning import Trainer, configure_logger
from agentlightning import Trainer, setup_logging
from agentlightning.litagent import rollout
from agentlightning.types import LLM, Dataset
@@ -173,5 +173,5 @@ def math_agent_dry_run() -> None:
if __name__ == "__main__":
configure_logger()
setup_logging()
math_agent_dry_run()
+2 -2
View File
@@ -32,7 +32,7 @@ from math_agent import GsmProblem, load_math_dataset
from rich.console import Console
from unsloth_helper import unsloth_training
from agentlightning import configure_logger
from agentlightning import setup_logging
from agentlightning.adapter import LlmProxyTraceToTriplet, TraceToTripletBase
from agentlightning.llm_proxy import LLMProxy, ModelConfig
from agentlightning.store import LightningStore, LightningStoreClient
@@ -380,7 +380,7 @@ async def sft_algorithm(*, store: LightningStore) -> None:
if __name__ == "__main__":
configure_logger()
setup_logging()
store = LightningStoreClient("http://localhost:4747")
+2 -2
View File
@@ -17,7 +17,7 @@ from math_agent import GsmProblem, load_math_dataset, math_agent
from rich.console import Console
from sft_algorithm import sft_one_iter
from agentlightning import Trainer, configure_logger
from agentlightning import Trainer, setup_logging
from agentlightning.adapter import TraceToTripletBase
from agentlightning.algorithm import Algorithm
from agentlightning.llm_proxy import LLMProxy
@@ -94,7 +94,7 @@ class UnslothSupervisedFinetuning(Algorithm):
if __name__ == "__main__":
configure_logger()
setup_logging()
algo = UnslothSupervisedFinetuning(
max_iterations=2,
+2 -2
View File
@@ -17,7 +17,7 @@ import multiprocessing
from math_agent import GsmProblem, math_agent
from rich.console import Console
from agentlightning import configure_logger
from agentlightning import setup_logging
from agentlightning.runner import LitAgentRunner
from agentlightning.store import LightningStore, LightningStoreClient
from agentlightning.tracer import OtelTracer
@@ -67,6 +67,6 @@ def spawn_runners(*, store: LightningStore, n_runners: int) -> None:
if __name__ == "__main__":
configure_logger()
setup_logging()
store = LightningStoreClient("http://localhost:4747")
spawn_runners(store=store, n_runners=4)
+420
View File
@@ -0,0 +1,420 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import io
import logging
import multiprocessing as mp
from multiprocessing.queues import Queue
from pathlib import Path
from typing import Any, Dict, List
import pytest
from agentlightning.logging import _to_level_value # pyright: ignore[reportPrivateUsage]
from agentlightning.logging import (
DATE_FORMAT,
DEFAULT_FORMAT,
)
def _logging_worker(case: str, queue: Queue[Dict[str, Any]]) -> None:
"""
Runs in a separate process using spawn. It performs a specific logging
configuration scenario and returns a summary dict via the queue.
"""
import logging
import warnings
# Re-import inside the subprocess so everything is picklable & isolated
from agentlightning.logging import (
setup,
setup_module,
)
if case == "setup_module_plain_console":
logger = setup_module(
level="DEBUG",
name="agentlightning.test",
console=True,
color=False,
propagate=False,
)
handlers = logger.handlers
handler = handlers[0] if handlers else None
fmt = handler.formatter._fmt if handler and handler.formatter else None
datefmt = handler.formatter.datefmt if handler and handler.formatter else None
queue.put(
{
"logger_name": logger.name,
"logger_level": logger.level,
"num_handlers": len(handlers),
"handler_class": handler.__class__.__name__ if handler else None,
"handler_level": handler.level if handler else None,
"fmt": fmt,
"datefmt": datefmt,
}
)
elif case == "setup_module_color_rich":
# Rich variant: color=True uses RichHandler
logger = setup_module(
level="INFO",
name="agentlightning.rich",
console=True,
color=True,
propagate=False,
)
handlers = logger.handlers
handler = handlers[0] if handlers else None
queue.put(
{
"logger_name": logger.name,
"logger_level": logger.level,
"num_handlers": len(handlers),
"handler_class": handler.__class__.__name__ if handler else None,
"handler_has_formatter": handler.formatter is not None if handler else None,
}
)
elif case == "setup_with_submodules_apply_to_capture_warnings":
# Extra handler to attach via extra_handlers
stream = io.StringIO()
stream_handler = logging.StreamHandler(stream)
setup(
level="INFO",
console=False,
color=False,
propagate=False,
disable_existing_loggers=False,
capture_warnings=True,
submodule_levels={"agentlightning.io": "DEBUG"},
extra_handlers=[stream_handler],
apply_to=["external"],
)
base = logging.getLogger("agentlightning")
sub = logging.getLogger("agentlightning.io")
ext = logging.getLogger("external")
# Capture warnings via logging after capture_warnings=True
class ListHandler(logging.Handler):
def __init__(self) -> None:
super().__init__()
self.records: List[logging.LogRecord] = []
def emit(self, record: logging.LogRecord) -> None:
self.records.append(record)
lh = ListHandler()
wlog = logging.getLogger("py.warnings")
wlog.handlers.clear()
wlog.addHandler(lh)
wlog.setLevel(logging.WARNING)
wlog.propagate = False
warnings.warn("from warnings", UserWarning)
queue.put(
{
"base_level": base.level,
"base_num_handlers": len(base.handlers),
"extra_in_base": stream_handler in base.handlers,
"sub_level": sub.level,
"ext_level": ext.level,
"ext_handlers_same": base.handlers == ext.handlers,
"ext_propagate": ext.propagate,
"warnings_logged": len(lh.records),
}
)
elif case == "setup_with_console_and_extra_handler":
# Console + extra handler combination to test handler attachment
stream = io.StringIO()
extra_handler = logging.StreamHandler(stream)
setup(
level="WARNING",
console=True,
color=False,
propagate=False,
extra_handlers=[extra_handler],
)
base = logging.getLogger("agentlightning")
handler_classes = [h.__class__.__name__ for h in base.handlers]
has_extra = extra_handler in base.handlers
queue.put(
{
"base_level": base.level,
"num_handlers": len(base.handlers),
"handler_classes": handler_classes,
"has_extra": has_extra,
}
)
else:
queue.put({})
def _logging_worker_files_string(queue: Queue[Dict[str, Any]], base_dir: str) -> None:
"""
Runs in a separate spawned process and configures logging with a single
files=str path. Returns information about the attached FileHandler.
"""
import logging
import os
from agentlightning.logging import setup
log_path = os.path.join(base_dir, "logs", "agent.log")
setup(
level="INFO",
console=False,
color=False,
propagate=False,
files=log_path,
)
base = logging.getLogger("agentlightning")
file_handlers = [h for h in base.handlers if isinstance(h, logging.FileHandler)]
fh = file_handlers[0] if file_handlers else None
fmt = fh.formatter._fmt if fh and fh.formatter else None
datefmt = fh.formatter.datefmt if fh and fh.formatter else None
queue.put(
{
"logger_level": base.level,
"num_handlers": len(base.handlers),
"num_file_handlers": len(file_handlers),
"file_base": fh.baseFilename if fh else None,
"file_level": fh.level if fh else None,
"fmt": fmt,
"datefmt": datefmt,
}
)
def _logging_worker_files_mapping(queue: Queue[Dict[str, Any]], base_dir: str) -> None:
"""
Runs in a separate spawned process and configures logging with a files=dict
mapping, then calls setup twice to verify idempotent FileHandler attachment.
"""
import logging
import os
from agentlightning.logging import setup
base_log = os.path.join(base_dir, "agent.log")
external_log = os.path.join(base_dir, "external.log")
files_mapping: Dict[str, str] = {
"agentlightning": base_log,
"external": external_log,
}
def file_handlers(logger: logging.Logger) -> list[logging.FileHandler]:
return [h for h in logger.handlers if isinstance(h, logging.FileHandler)]
# First setup call
setup(
level="DEBUG",
console=False,
color=False,
propagate=False,
files=files_mapping,
)
base_logger = logging.getLogger("agentlightning")
ext_logger = logging.getLogger("external")
base_fh_first = file_handlers(base_logger)
ext_fh_first = file_handlers(ext_logger)
# Second setup call with the same mapping should not add duplicate FileHandlers
setup(
level="DEBUG",
console=False,
color=False,
propagate=False,
files=files_mapping,
)
base_fh_second = file_handlers(base_logger)
ext_fh_second = file_handlers(ext_logger)
queue.put(
{
"base_level": base_logger.level,
"ext_level": ext_logger.getEffectiveLevel(),
"base_first_count": len(base_fh_first),
"ext_first_count": len(ext_fh_first),
"base_second_count": len(base_fh_second),
"ext_second_count": len(ext_fh_second),
"base_file_first": base_fh_first[0].baseFilename if base_fh_first else None,
"ext_file_first": ext_fh_first[0].baseFilename if ext_fh_first else None,
"base_file_second": base_fh_second[0].baseFilename if base_fh_second else None,
"ext_file_second": ext_fh_second[0].baseFilename if ext_fh_second else None,
# For sanity: capture handler levels as well
"base_handler_level": base_fh_first[0].level if base_fh_first else None,
"ext_handler_level": ext_fh_first[0].level if ext_fh_first else None,
}
)
def _run_case(case: str) -> Dict[str, Any]:
"""Helper to run a scenario in a spawned process and fetch the result."""
ctx = mp.get_context("spawn")
q: Queue[Dict[str, Any]] = ctx.Queue()
p = ctx.Process(target=_logging_worker, args=(case, q))
p.start()
result = q.get(timeout=10)
p.join(timeout=10)
assert p.exitcode == 0
return result
def test_to_level_value_int_and_str() -> None:
# direct, no multiprocessing needed
assert _to_level_value(logging.DEBUG) == logging.DEBUG
assert _to_level_value("info") == logging.INFO
assert _to_level_value("WARNING") == logging.WARNING
with pytest.raises(ValueError):
_to_level_value("not-a-level")
def test_setup_module_plain_console_spawn() -> None:
result = _run_case("setup_module_plain_console")
assert result["logger_name"] == "agentlightning.test"
assert result["logger_level"] == logging.DEBUG
# Console handler with plain formatter configured
assert result["num_handlers"] == 1
assert result["handler_class"].endswith("StreamHandler")
assert result["handler_level"] == logging.DEBUG
assert result["fmt"] == DEFAULT_FORMAT
assert result["datefmt"] == DATE_FORMAT
def test_setup_module_color_rich_spawn() -> None:
# Only run this test if rich is installed
pytest.importorskip("rich")
result = _run_case("setup_module_color_rich")
assert result["logger_name"] == "agentlightning.rich"
assert result["logger_level"] == logging.INFO
assert result["num_handlers"] == 1
# We cant rely on full module path, just the class name
assert result["handler_class"].endswith("RichHandler")
def test_setup_with_submodules_apply_to_and_capture_warnings_spawn() -> None:
result = _run_case("setup_with_submodules_apply_to_capture_warnings")
# Base logger level and handler attachment
assert result["base_level"] == logging.INFO
assert result["base_num_handlers"] >= 1
assert result["extra_in_base"] is True
# Submodule level overridden
assert result["sub_level"] == logging.DEBUG
# apply_to logger mirrors base handlers & level, propagation disabled
assert result["ext_level"] == logging.INFO
assert result["ext_handlers_same"] is True
assert result["ext_propagate"] is False
# capture_warnings=True causes warnings.warn to go through logging
assert result["warnings_logged"] >= 1
def test_setup_with_console_and_extra_handler_spawn() -> None:
result = _run_case("setup_with_console_and_extra_handler")
# Level propagated to base logger
assert result["base_level"] == logging.WARNING
# Both console handler and extra handler should be attached
assert result["num_handlers"] >= 2
assert any(cls.endswith("StreamHandler") for cls in result["handler_classes"])
assert result["has_extra"] is True
def test_setup_files_string_spawn(tmp_path: Path) -> None:
"""
Verifies that passing files as a string attaches a single FileHandler with
the expected level and default formatter in a spawned process.
"""
ctx = mp.get_context("spawn")
q: Queue[Dict[str, Any]] = ctx.Queue()
p = ctx.Process(target=_logging_worker_files_string, args=(q, str(tmp_path)))
p.start()
result = q.get(timeout=10)
p.join(timeout=10)
assert p.exitcode == 0
assert result["logger_level"] == logging.INFO
# We expect at least one handler and exactly one FileHandler
assert result["num_handlers"] >= 1
assert result["num_file_handlers"] == 1
# Filename should be inside the tmp_path tree
assert str(tmp_path) in result["file_base"]
# FileHandler uses the base logger level
assert result["file_level"] == logging.INFO
# Default formatter applied by _ensure_file_handler
assert result["fmt"] == DEFAULT_FORMAT
assert result["datefmt"] == DATE_FORMAT
def test_setup_files_mapping_spawn(tmp_path: Path) -> None:
"""
Verifies that passing files as a mapping attaches FileHandlers to each
logger and that calling setup twice does not create duplicate handlers.
"""
ctx = mp.get_context("spawn")
q: Queue[Dict[str, Any]] = ctx.Queue()
p = ctx.Process(target=_logging_worker_files_mapping, args=(q, str(tmp_path)))
p.start()
result = q.get(timeout=10)
p.join(timeout=10)
assert p.exitcode == 0
# Base logger level is DEBUG
assert result["base_level"] == logging.DEBUG
# External's effective level is WARNING (inherited from root)
assert result["ext_level"] == logging.WARNING
# First setup: one FileHandler per logger
assert result["base_first_count"] == 1
assert result["ext_first_count"] == 1
# Second setup: still one FileHandler per logger (idempotence)
assert result["base_second_count"] == 1
assert result["ext_second_count"] == 1
# File paths are stable across calls
assert result["base_file_first"] == result["base_file_second"]
assert result["ext_file_first"] == result["ext_file_second"]
# Paths should live under tmp_path
assert str(tmp_path) in result["base_file_first"]
assert str(tmp_path) in result["ext_file_first"]
# Handler levels:
# - base handler uses the base logger level (DEBUG)
# - external handler uses external's effective level at creation (WARNING)
assert result["base_handler_level"] == logging.DEBUG
assert result["ext_handler_level"] == logging.WARNING
-3
View File
@@ -791,9 +791,6 @@ async def test_run_gunicorn_reports_health_failure_preload():
"""
Health endpoint returns 503 -> watchdog posts error and requests graceful shutdown.
"""
from agentlightning.logging import configure_logger
configure_logger(logging.DEBUG)
host = "127.0.0.1"
port = portpicker.pick_unused_port()
ctx = multiprocessing.get_context("fork")