85742ab165
CPU Test / Lint - next (push) Waiting to run
Dashboard / Chromatic (push) Waiting to run
CPU Test / Lint - fast (push) Waiting to run
CPU Test / Build documentation (push) Waiting to run
CPU Test / Test (Store, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Weave, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.11) (push) Waiting to run
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Others, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Store, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Utilities, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (Weave, stable, Python 3.12) (push) Waiting to run
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Others, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Store, latest, Python 3.13) (push) Waiting to run
CPU Test / Lint - slow (push) Waiting to run
CPU Test / Lint - JavaScript (push) Waiting to run
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Others, legacy, Python 3.10) (push) Waiting to run
CPU Test / Test (Utilities, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (Weave, latest, Python 3.13) (push) Waiting to run
CPU Test / Test (JavaScript) (push) Waiting to run
Deploy Documentation / deploy (push) Has been cancelled
371 lines
13 KiB
Python
371 lines
13 KiB
Python
# 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
|
|
|
|
from rich.console import Console
|
|
|
|
__all__ = ["setup", "configure_logger", "setup_module"]
|
|
|
|
|
|
def configure_logger(level: int = logging.INFO, name: str = "agentlightning") -> logging.Logger:
|
|
"""Create or reset a namespaced logger with a consistent console format.
|
|
|
|
This helper clears any previously attached handlers before binding a single
|
|
`StreamHandler` that writes to standard output. The resulting logger does
|
|
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`.
|
|
name: Dotted path for the logger instance. Defaults to
|
|
`"agentlightning"`.
|
|
|
|
Returns:
|
|
Configured logger instance ready for immediate use.
|
|
|
|
Examples:
|
|
```python
|
|
from agentlightning import configure_logger
|
|
|
|
logger = configure_logger(level=logging.INFO)
|
|
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 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"
|
|
|
|
base_logger = setup_module(
|
|
level,
|
|
name="agentlightning",
|
|
console=console,
|
|
color=color,
|
|
propagate=propagate,
|
|
disable_existing_loggers=disable_existing_loggers,
|
|
)
|
|
|
|
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()
|