diff --git a/server/.env.example b/server/.env.example index 6d9aaef7..4e33dddf 100644 --- a/server/.env.example +++ b/server/.env.example @@ -50,3 +50,21 @@ AGENT_CONTROL_DB_PORT=5432 AGENT_CONTROL_DB_USER=agent_control AGENT_CONTROL_DB_PASSWORD=agent_control AGENT_CONTROL_DB_DATABASE=agent_control + +########### +# Logging # +########### + +# Log level for the server's own loggers (e.g. INFO, DEBUG, WARNING). +# AGENT_CONTROL_LOG_LEVEL=INFO + +# Emit logs as JSON instead of plain text. +# AGENT_CONTROL_LOG_JSON=false + +# Whether the server installs its own logging handlers. Set to false to let an +# embedding host own the logging configuration (passes log_config=None to uvicorn). +# AGENT_CONTROL_CONFIGURE_LOGGING=true + +# Whether uvicorn's built-in per-request access log is emitted. Set to false to +# suppress it, e.g. when an embedding host already emits its own request log. +# AGENT_CONTROL_ACCESS_LOG=true diff --git a/server/src/agent_control_server/config.py b/server/src/agent_control_server/config.py index fe481881..e9e62f9e 100644 --- a/server/src/agent_control_server/config.py +++ b/server/src/agent_control_server/config.py @@ -280,6 +280,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") + access_log: bool = _env_alias_field(True, "AGENT_CONTROL_ACCESS_LOG") level: str | None = None json_logs: bool = _env_alias_field(False, "AGENT_CONTROL_LOG_JSON") diff --git a/server/src/agent_control_server/logging_utils.py b/server/src/agent_control_server/logging_utils.py index c41d4280..d9eeb9ce 100644 --- a/server/src/agent_control_server/logging_utils.py +++ b/server/src/agent_control_server/logging_utils.py @@ -71,6 +71,11 @@ def should_configure_logging() -> bool: return LoggingSettings().configure_logging +def access_log_enabled() -> bool: + """Return whether uvicorn's per-request access log should be emitted.""" + return LoggingSettings().access_log + + def configure_logging( *, level: str | int | None = None, diff --git a/server/src/agent_control_server/main.py b/server/src/agent_control_server/main.py index 16152824..3cbe9392 100644 --- a/server/src/agent_control_server/main.py +++ b/server/src/agent_control_server/main.py @@ -37,7 +37,12 @@ http_exception_handler, validation_exception_handler, ) -from .logging_utils import configure_logging, get_uvicorn_log_level_name, should_configure_logging +from .logging_utils import ( + access_log_enabled, + configure_logging, + get_uvicorn_log_level_name, + should_configure_logging, +) from .observability.ingest import DirectEventIngestor from .observability.sinks import ( EventStoreControlEventSink, @@ -417,6 +422,7 @@ def run() -> None: "host": settings.host, "port": settings.port, "log_level": get_uvicorn_log_level_name(_default_log_level()).lower(), + "access_log": access_log_enabled(), } if not should_configure_logging(): uvicorn_kwargs["log_config"] = None diff --git a/server/tests/test_config.py b/server/tests/test_config.py index 2a9bd472..62157fae 100644 --- a/server/tests/test_config.py +++ b/server/tests/test_config.py @@ -287,3 +287,17 @@ def test_logging_settings_supports_host_owned_logging(monkeypatch) -> None: config = LoggingSettings() assert config.configure_logging is False + + +def test_logging_settings_access_log_defaults_to_true() -> None: + config = LoggingSettings() + + assert config.access_log is True + + +def test_logging_settings_access_log_can_be_disabled(monkeypatch) -> None: + monkeypatch.setenv("AGENT_CONTROL_ACCESS_LOG", "false") + + config = LoggingSettings() + + assert config.access_log is False diff --git a/server/tests/test_logging_utils.py b/server/tests/test_logging_utils.py index 5ed2e001..a816c091 100644 --- a/server/tests/test_logging_utils.py +++ b/server/tests/test_logging_utils.py @@ -5,6 +5,7 @@ from agent_control_server.logging_utils import ( _parse_json, _parse_level, + access_log_enabled, configure_logging, get_log_level_name, get_uvicorn_log_level_name, @@ -147,6 +148,16 @@ def test_should_configure_logging_can_be_disabled(monkeypatch) -> None: assert should_configure_logging() is False +def test_access_log_enabled_defaults_to_true() -> None: + assert access_log_enabled() is True + + +def test_access_log_enabled_can_be_disabled(monkeypatch) -> None: + monkeypatch.setenv("AGENT_CONTROL_ACCESS_LOG", "false") + + assert access_log_enabled() is False + + def test_configure_logging_noops_when_host_owns_logging(monkeypatch) -> None: monkeypatch.setenv("AGENT_CONTROL_CONFIGURE_LOGGING", "false") diff --git a/server/tests/test_main_lifespan.py b/server/tests/test_main_lifespan.py index 293fb957..33c9554e 100644 --- a/server/tests/test_main_lifespan.py +++ b/server/tests/test_main_lifespan.py @@ -273,10 +273,11 @@ def test_run_uses_settings(monkeypatch) -> None: # Given: patched settings and uvicorn.run called = {} - def fake_run(app, host, port, log_level): + def fake_run(app, host, port, log_level, access_log): called["host"] = host called["port"] = port called["log_level"] = log_level + called["access_log"] = access_log monkeypatch.setattr(main_module.uvicorn, "run", fake_run) monkeypatch.setattr(settings, "host", "127.0.0.1") @@ -290,6 +291,7 @@ def fake_run(app, host, port, log_level): assert called["host"] == "127.0.0.1" assert called["port"] == 9999 assert called["log_level"] == "debug" + assert called["access_log"] is True def test_run_disables_uvicorn_log_config_when_host_owns_logging(monkeypatch) -> None: @@ -307,3 +309,20 @@ def fake_run(app, **kwargs): # type: ignore[no-untyped-def] main_module.run() assert called["log_config"] is None + + +def test_run_disables_uvicorn_access_log_when_requested(monkeypatch) -> None: + called = {} + + def fake_run(app, **kwargs): # type: ignore[no-untyped-def] + called.update(kwargs) + + monkeypatch.setenv("AGENT_CONTROL_ACCESS_LOG", "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["access_log"] is False