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
5 changes: 5 additions & 0 deletions server/src/agent_control_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ class LoggingSettings(BaseSettings):

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

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

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


def should_configure_logging() -> bool:
"""Return whether Agent Control should install its own logging handlers."""
return LoggingSettings().configure


def configure_logging(
*,
level: str | int | None = None,
Expand Down
11 changes: 8 additions & 3 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 @@ -88,6 +88,11 @@ def add_prometheus_metrics(app: FastAPI, metrics_prefix: str) -> None:
app.add_route(METRICS_PATH, handle_metrics)


def _configure_process_logging() -> None:
if should_configure_logging():
configure_logging(default_level=_default_log_level())


async def _shutdown_observability_sink(sink: object) -> None:
"""Flush and close a custom async sink when it exposes lifecycle hooks."""
flush = getattr(sink, "flush", None)
Expand Down Expand Up @@ -120,7 +125,7 @@ async def _shutdown_observability_sink(sink: object) -> None:
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Lifespan context manager for FastAPI app startup and shutdown."""
# Startup: Configure logging
configure_logging(default_level=_default_log_level())
_configure_process_logging()

# Install the request-auth provider selected by environment variables.
from .auth_framework.config import configure_auth_from_env
Expand Down Expand Up @@ -231,7 +236,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
)

# Configure logging
configure_logging(default_level=_default_log_level())
_configure_process_logging()


@app.middleware("http")
Expand Down
21 changes: 21 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 @@ -77,6 +78,26 @@ def test_parse_json_treats_blank_env_value_as_false(monkeypatch) -> None:
assert parsed is False


def test_should_configure_logging_defaults_to_true() -> None:
assert should_configure_logging() is True


def test_should_configure_logging_reads_host_override(monkeypatch) -> None:
# Given: the embedding host opts out of Agent Control's logging setup
monkeypatch.setenv("AGENT_CONTROL_CONFIGURE_LOGGING", "false")

# When/then: Agent Control should leave logging handlers to the host
assert should_configure_logging() is False


def test_should_configure_logging_supports_log_prefixed_alias(monkeypatch) -> None:
# Given: the logging-prefixed alias is used
monkeypatch.setenv("AGENT_CONTROL_LOG_CONFIGURE", "false")

# When/then: the alias disables Agent Control logging setup
assert should_configure_logging() is False


def test_get_log_level_name_falls_back_to_default_for_invalid_env(monkeypatch) -> None:
# Given: AGENT_CONTROL_LOG_LEVEL is present but invalid
monkeypatch.setenv("AGENT_CONTROL_LOG_LEVEL", "not-a-level")
Expand Down
29 changes: 29 additions & 0 deletions server/tests/test_main_lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,35 @@ def test_lifespan_skips_observability_when_disabled(monkeypatch) -> None:
assert not hasattr(app.state, "event_ingestor")


def test_configure_process_logging_respects_host_override(monkeypatch) -> None:
calls = {"count": 0}

def fake_configure_logging(*, default_level: str) -> None:
calls["count"] += 1

monkeypatch.setattr(main_module, "should_configure_logging", lambda: False)
monkeypatch.setattr(main_module, "configure_logging", fake_configure_logging)

main_module._configure_process_logging()

assert calls["count"] == 0


def test_configure_process_logging_uses_default_level(monkeypatch) -> None:
calls = {}

def fake_configure_logging(*, default_level: str) -> None:
calls["default_level"] = default_level

monkeypatch.setattr(main_module, "should_configure_logging", lambda: True)
monkeypatch.setattr(main_module, "configure_logging", fake_configure_logging)
monkeypatch.setattr(settings, "debug", True)

main_module._configure_process_logging()

assert calls == {"default_level": "DEBUG"}


def test_custom_openapi_replaces_jsonvalue(monkeypatch) -> None:
# Given: a custom openapi generator that includes JSONValue
def fake_get_openapi(*, title, version, description, routes):
Expand Down
Loading