Skip to content
Open
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
18 changes: 18 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -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")

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 @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion server/src/agent_control_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions server/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions server/tests/test_logging_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")

Expand Down
21 changes: 20 additions & 1 deletion server/tests/test_main_lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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:
Expand All @@ -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
Loading