Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/src/agent_control_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ class LoggingSettings(BaseSettings):

model_config = SettingsConfigDict(**_COMMON_SETTINGS_CONFIG, env_prefix="AGENT_CONTROL_LOG_")

configure_logging: bool = _env_alias_field(True, "AGENT_CONTROL_CONFIGURE_LOGGING")
level: str | None = None
json_logs: bool = _env_alias_field(False, "AGENT_CONTROL_LOG_JSON")

Expand Down
8 changes: 8 additions & 0 deletions server/src/agent_control_server/logging_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,20 @@ def _parse_json(json_flag: bool | None) -> bool:
return LoggingSettings().json_logs


def should_configure_logging() -> bool:
"""Return whether the server should install its own logging handlers."""
return LoggingSettings().configure_logging


def configure_logging(
*,
level: str | int | None = None,
json: bool | None = None,
default_level: str = "INFO",
) -> None:
if not should_configure_logging():
return

resolved_level = level if level is not None else get_log_level_name(default_level)
lvl = _parse_level(resolved_level)
as_json = _parse_json(json)
Expand Down
17 changes: 10 additions & 7 deletions server/src/agent_control_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
http_exception_handler,
validation_exception_handler,
)
from .logging_utils import configure_logging, get_uvicorn_log_level_name
from .logging_utils import configure_logging, get_uvicorn_log_level_name, should_configure_logging
from .observability.ingest import DirectEventIngestor
from .observability.sinks import (
EventStoreControlEventSink,
Expand Down Expand Up @@ -403,12 +403,15 @@ async def health_check() -> HealthResponse:

def run() -> None:
"""Run the server application."""
uvicorn.run(
app,
host=settings.host,
port=settings.port,
log_level=get_uvicorn_log_level_name(_default_log_level()).lower(),
)
uvicorn_kwargs: dict[str, Any] = {
"host": settings.host,
"port": settings.port,
"log_level": get_uvicorn_log_level_name(_default_log_level()).lower(),
}
if not should_configure_logging():
uvicorn_kwargs["log_config"] = None

uvicorn.run(app, **uvicorn_kwargs)


if __name__ == "__main__":
Expand Down
15 changes: 15 additions & 0 deletions server/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from agent_control_server.config import (
AgentControlServerDatabaseConfig,
LoggingSettings,
ObservabilitySettings,
Settings,
)
Expand Down Expand Up @@ -230,3 +231,17 @@ def test_observability_settings_ignore_legacy_env_vars(monkeypatch) -> None:
# Then: the legacy env vars are ignored
assert config.enabled is True
assert config.stdout is False


def test_logging_settings_configure_logging_defaults_to_true() -> None:
config = LoggingSettings()

assert config.configure_logging is True


def test_logging_settings_supports_host_owned_logging(monkeypatch) -> None:
monkeypatch.setenv("AGENT_CONTROL_CONFIGURE_LOGGING", "false")

config = LoggingSettings()

assert config.configure_logging is False
27 changes: 27 additions & 0 deletions server/tests/test_logging_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
configure_logging,
get_log_level_name,
get_uvicorn_log_level_name,
should_configure_logging,
)


Expand Down Expand Up @@ -138,3 +139,29 @@ def test_configure_logging_resets_uvicorn_handlers() -> None:
logger.propagate = original_propagate
root.handlers = root_handlers
root.setLevel(root_level)


def test_should_configure_logging_can_be_disabled(monkeypatch) -> None:
monkeypatch.setenv("AGENT_CONTROL_CONFIGURE_LOGGING", "false")

assert should_configure_logging() is False


def test_configure_logging_noops_when_host_owns_logging(monkeypatch) -> None:
monkeypatch.setenv("AGENT_CONTROL_CONFIGURE_LOGGING", "false")

root = logging.getLogger()
handler = logging.StreamHandler()
original_handlers = list(root.handlers)
original_level = root.level
root.handlers = [handler]
root.setLevel(logging.ERROR)

try:
configure_logging(level="INFO", json=False)

assert root.handlers == [handler]
assert root.level == logging.ERROR
finally:
root.handlers = original_handlers
root.setLevel(original_level)
22 changes: 19 additions & 3 deletions server/tests/test_main_lifespan.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from __future__ import annotations

from fastapi import FastAPI
from fastapi.testclient import TestClient

from agent_control_server import main as main_module
from agent_control_server.config import observability_settings, settings
from agent_control_server.main import lifespan
Expand All @@ -11,6 +8,8 @@
register_control_event_sink_factory,
unregister_control_event_sink_factory,
)
from fastapi import FastAPI
from fastapi.testclient import TestClient


def test_lifespan_initializes_observability_when_enabled(monkeypatch) -> None:
Expand Down Expand Up @@ -230,3 +229,20 @@ def fake_run(app, host, port, log_level):
assert called["host"] == "127.0.0.1"
assert called["port"] == 9999
assert called["log_level"] == "debug"


def test_run_disables_uvicorn_log_config_when_host_owns_logging(monkeypatch) -> None:
called = {}

def fake_run(app, **kwargs): # type: ignore[no-untyped-def]
called.update(kwargs)

monkeypatch.setenv("AGENT_CONTROL_CONFIGURE_LOGGING", "false")
monkeypatch.setattr(main_module.uvicorn, "run", fake_run)
monkeypatch.setattr(settings, "host", "127.0.0.1")
monkeypatch.setattr(settings, "port", 9999)
monkeypatch.setattr(settings, "debug", False)

main_module.run()

assert called["log_config"] is None
Loading