Skip to content

Commit 256f7fc

Browse files
authored
fix(ohmo): tee gateway run logs to file (#288)
1 parent a41011d commit 256f7fc

4 files changed

Lines changed: 98 additions & 4 deletions

File tree

ohmo/cli.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from ohmo.session_storage import OhmoSessionBackend
2626
from ohmo.workspace import (
2727
get_gateway_config_path,
28+
get_logs_dir,
2829
get_workspace_root,
2930
get_soul_path,
3031
get_state_path,
@@ -407,18 +408,42 @@ def _maybe_restart_gateway(*, cwd: str | Path, workspace: str | Path) -> None:
407408
print(f"ohmo gateway restarted (pid={pid})")
408409

409410

410-
def _configure_gateway_logging(workspace: str | Path | None = None) -> None:
411+
def _configure_gateway_logging(
412+
workspace: str | Path | None = None,
413+
*,
414+
console: bool = True,
415+
log_file: bool = True,
416+
) -> None:
411417
"""Configure foreground gateway logging."""
412418
config = load_gateway_config(workspace)
413419
level_name = str(config.log_level or "INFO").upper()
414420
level = getattr(logging, level_name, logging.INFO)
421+
handlers = _build_gateway_logging_handlers(workspace, console=console, log_file=log_file)
415422
logging.basicConfig(
416423
level=level,
417424
format="%(asctime)s [%(name)s] %(levelname)s %(message)s",
425+
handlers=handlers,
418426
force=True,
419427
)
420428

421429

430+
def _build_gateway_logging_handlers(
431+
workspace: str | Path | None = None,
432+
*,
433+
console: bool,
434+
log_file: bool,
435+
) -> list[logging.Handler]:
436+
"""Build gateway log handlers for foreground and daemon modes."""
437+
handlers: list[logging.Handler] = []
438+
if console:
439+
handlers.append(logging.StreamHandler())
440+
if log_file:
441+
log_path = get_logs_dir(workspace) / "gateway.log"
442+
log_path.parent.mkdir(parents=True, exist_ok=True)
443+
handlers.append(logging.FileHandler(log_path, encoding="utf-8", delay=True))
444+
return handlers
445+
446+
422447
@app.callback(invoke_without_command=True)
423448
def main(
424449
ctx: typer.Context,
@@ -637,9 +662,11 @@ def user_edit_cmd(
637662
def gateway_run_cmd(
638663
cwd: str = typer.Option(str(Path.cwd()), "--cwd", help="Project working directory"),
639664
workspace: str | None = typer.Option(None, "--workspace", help=_WORKSPACE_HELP),
665+
console_log: bool = typer.Option(True, "--console-log/--no-console-log", hidden=True),
666+
log_file: bool = typer.Option(True, "--log-file/--no-log-file", hidden=True),
640667
) -> None:
641668
"""Run the ohmo gateway in the foreground."""
642-
_configure_gateway_logging(workspace)
669+
_configure_gateway_logging(workspace, console=console_log, log_file=log_file)
643670
service = OhmoGatewayService(cwd, workspace)
644671
raise SystemExit(asyncio.run(service.run_foreground()))
645672

ohmo/gateway/service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ def start_gateway_process(cwd: str | Path | None = None, workspace: str | Path |
290290
service._cwd,
291291
"--workspace",
292292
str(get_workspace_root(workspace)),
293+
"--no-console-log",
293294
],
294295
**popen_kwargs,
295296
)

tests/test_ohmo/test_cli.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import json
2+
import logging
23
from pathlib import Path
34

45
from typer.testing import CliRunner
56

6-
from ohmo.cli import app
7+
from ohmo.cli import _build_gateway_logging_handlers, app
78

89

910
def test_ohmo_help():
@@ -48,6 +49,42 @@ def test_ohmo_init_noninteractive_defaults_to_deny_all_remote_access(tmp_path: P
4849
assert config["channel_configs"] == {}
4950

5051

52+
def test_gateway_logging_handlers_write_gateway_log_file(tmp_path: Path):
53+
workspace = tmp_path / ".ohmo-home"
54+
runner = CliRunner()
55+
result = runner.invoke(app, ["init", "--workspace", str(workspace), "--no-interactive"])
56+
assert result.exit_code == 0
57+
58+
handlers = _build_gateway_logging_handlers(workspace, console=True, log_file=True)
59+
try:
60+
file_handlers = [handler for handler in handlers if isinstance(handler, logging.FileHandler)]
61+
console_handlers = [
62+
handler
63+
for handler in handlers
64+
if isinstance(handler, logging.StreamHandler) and not isinstance(handler, logging.FileHandler)
65+
]
66+
assert len(file_handlers) == 1
67+
assert len(console_handlers) == 1
68+
69+
record = logging.LogRecord(
70+
name="ohmo.gateway.test",
71+
level=logging.INFO,
72+
pathname=__file__,
73+
lineno=1,
74+
msg="GATEWAY_LOG_OK",
75+
args=(),
76+
exc_info=None,
77+
)
78+
file_handlers[0].emit(record)
79+
file_handlers[0].flush()
80+
81+
log_path = workspace / "logs" / "gateway.log"
82+
assert "GATEWAY_LOG_OK" in log_path.read_text(encoding="utf-8")
83+
finally:
84+
for handler in handlers:
85+
handler.close()
86+
87+
5188
def test_ohmo_init_interactive_writes_gateway_config(tmp_path: Path, monkeypatch):
5289
runner = CliRunner()
5390
workspace = tmp_path / ".ohmo-home"

tests/test_ohmo/test_gateway.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import contextlib
33
import logging
44
import subprocess
5+
import sys
56
from types import SimpleNamespace
67
from datetime import datetime
78
import json
@@ -46,7 +47,7 @@
4647
_sanitize_group_command_metadata,
4748
_sanitize_group_command_prompts,
4849
)
49-
from ohmo.gateway.service import OhmoGatewayService, gateway_status, stop_gateway_process
50+
from ohmo.gateway.service import OhmoGatewayService, gateway_status, start_gateway_process, stop_gateway_process
5051
from ohmo.group_registry import load_managed_group_record, save_managed_group_record
5152
from ohmo.memory import add_memory_entry as add_ohmo_memory_entry
5253
from ohmo.memory import list_memory_files as list_ohmo_memory_files
@@ -423,6 +424,34 @@ def test_gateway_status_prefers_live_config_over_stale_state(tmp_path):
423424
assert state.enabled_channels == ["feishu"]
424425

425426

427+
def test_start_gateway_process_uses_child_log_file_handler_without_console_duplication(tmp_path, monkeypatch):
428+
workspace = tmp_path / ".ohmo-home"
429+
initialize_workspace(workspace)
430+
captured: dict[str, object] = {}
431+
432+
class FakeProcess:
433+
pid = 1234
434+
435+
def fake_popen(args, **kwargs):
436+
captured["args"] = args
437+
captured["kwargs"] = kwargs
438+
return FakeProcess()
439+
440+
monkeypatch.setattr("ohmo.gateway.service.subprocess.Popen", fake_popen)
441+
442+
assert start_gateway_process(tmp_path, workspace) == 1234
443+
444+
args = captured["args"]
445+
kwargs = captured["kwargs"]
446+
assert isinstance(args, list)
447+
assert args[:4] == [sys.executable, "-m", "ohmo", "gateway"]
448+
assert "run" in args
449+
assert "--no-console-log" in args
450+
assert isinstance(kwargs, dict)
451+
assert kwargs["stdout"] is kwargs["stderr"]
452+
assert getattr(kwargs["stdout"], "name", "").endswith("gateway.log")
453+
454+
426455
def test_stop_gateway_process_kills_matching_workspace_processes(tmp_path, monkeypatch):
427456
workspace = tmp_path / ".ohmo-home"
428457
workspace.mkdir()

0 commit comments

Comments
 (0)