Skip to content

Commit e3a47d0

Browse files
committed
refactor(logs): restructure log layout and rename log config
Reorganize per-instance logs into one self-contained directory and disambiguate the two logging concepts that previously shared the BALATROBOT_PATH_LOGS name. New layout (previously flat with port-prefixed filenames at the session level): logs/<timestamp>/<port>/ ├── balatro.log process stdout/stderr ├── requests.jsonl API request trace ├── responses.jsonl API response trace └── screenshots/<id>.png The env var is split into two distinct names: - BALATROBOT_LOGS (--logs, config.logs): user-facing parent dir, Python-only. No longer emitted to the subprocess, since the Lua mod never read it. - BALATROBOT_LOG_DIR: per-instance dir, set imperatively by the launcher, read by the Lua mod. Replaces the previous pattern of emitting BALATROBOT_PATH_LOGS as input then overwriting it with a more specific value. InstanceInfo.log_path still points at the log file (now .../<port>/balatro.log), so `balatrobot list --json` is unchanged. Closes #211
1 parent 2a1d1e3 commit e3a47d0

15 files changed

Lines changed: 62 additions & 54 deletions

File tree

.agents/skills/balatrobot/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ balatrobot stop # SIGTERM + 5s poll, cleans state file
6565

6666
## Logs
6767

68-
Session directory `logs/<timestamp>/` contains `<port>.log`, `<port>.req.jsonl`, `<port>.res.jsonl`. Find paths via `balatrobot list --json`.
68+
Session directory `logs/<timestamp>/` holds one subdir per instance (`<port>/`), each containing `balatro.log`, `requests.jsonl`, `responses.jsonl`, and `screenshots/<id>.png`. Find paths via `balatrobot list --json`.

docs/cli.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ All options can be set via CLI flags or environment variables. CLI flags overrid
4545
| `--path-lovely PATH` | `BALATROBOT_PATH_LOVELY` | auto-detected | Path to lovely library (dll/so/dylib) |
4646
| `--path-love PATH` | `BALATROBOT_PATH_LOVE` | auto-detected | Path to game launcher executable |
4747
| `--platform PLATFORM` | `BALATROBOT_PLATFORM` | auto-detected | Platform: `darwin`, `linux`, `windows`, `native` |
48-
| `--path-logs PATH` | `BALATROBOT_PATH_LOGS` | `logs` | Directory for log files |
48+
| `--logs PATH` | `BALATROBOT_LOGS` | `logs` | Log directory (parent of timestamped sessions) |
4949
| `-h, --help` | - | - | Show help message and exit |
5050

5151
### Render Modes
@@ -173,7 +173,7 @@ uvx balatrobot serve
173173

174174
The CLI automatically:
175175

176-
- Logs output to `logs/{timestamp}/{port}.log`
176+
- Logs output to `logs/{timestamp}/{port}/balatro.log`
177177
- Sets up the correct environment variables
178178
- Gracefully shuts down on Ctrl+C
179179

@@ -310,7 +310,7 @@ uvx balatrobot serve --platform native --path-balatro /path/to/balatro/source
310310

311311
## Troubleshooting
312312

313-
**Connection refused**: Ensure Balatro is running and the mod loaded successfully. Check logs in `logs/{timestamp}/{port}.log` for errors. Verify the in-game profile is named exactly `"BalatroBot"`.
313+
**Connection refused**: Ensure Balatro is running and the mod loaded successfully. Check logs in `logs/{timestamp}/{port}/balatro.log` for errors. Verify the in-game profile is named exactly `"BalatroBot"`.
314314

315315
**Mod not loading**: Verify that Lovely Injector and Steamodded are installed correctly. Ensure you have a Balatro profile named `"BalatroBot"` and it is selected.
316316

src/balatrobot/cli/serve.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ def serve(
144144
str | None,
145145
typer.Option("--platform", help="Platform (darwin, linux, windows, native)"),
146146
] = None,
147-
path_logs: Annotated[
148-
str | None, typer.Option("--path-logs", help="Log directory")
147+
logs: Annotated[
148+
str | None, typer.Option("--logs", help="Log directory (parent of sessions)")
149149
] = None,
150150
# fmt: on
151151
) -> None:
@@ -175,7 +175,7 @@ def serve(
175175
path_lovely=path_lovely,
176176
path_love=path_love,
177177
platform=platform,
178-
path_logs=path_logs,
178+
logs=logs,
179179
)
180180
except ValueError as e:
181181
typer.echo(f"Error: {e}", err=True)
@@ -200,7 +200,7 @@ async def _serve(config: Config, n: int) -> None:
200200
for i, info in enumerate(pool.instances):
201201
typer.echo(f"Instance [{i}]: {info.url}")
202202
typer.echo(
203-
f"Session: {pool.session_name} | Logs: {config.path_logs or 'logs'}/{pool.session_name}/"
203+
f"Session: {pool.session_name} | Logs: {config.logs or 'logs'}/{pool.session_name}/"
204204
)
205205
typer.echo("Press Ctrl+C to stop.")
206206
await server.run()

src/balatrobot/config.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"path_lovely": "BALATROBOT_PATH_LOVELY",
1515
"path_love": "BALATROBOT_PATH_LOVE",
1616
"platform": "BALATROBOT_PLATFORM",
17-
"path_logs": "BALATROBOT_PATH_LOGS",
17+
"logs": "BALATROBOT_LOGS",
1818
}
1919

2020
RENDER_CHOICES = frozenset({"headfull", "headless", "ondemand"})
@@ -52,7 +52,7 @@ class Config:
5252
path_lovely: str | None = None
5353
path_love: str | None = None
5454
platform: str | None = None
55-
path_logs: str | None = None
55+
logs: str | None = None
5656

5757
def __post_init__(self) -> None:
5858
if self.render not in RENDER_CHOICES:
@@ -89,6 +89,11 @@ def to_env(self) -> dict[str, str]:
8989
"""Convert config to environment variables dict."""
9090
env: dict[str, str] = {}
9191
for field, env_var in ENV_MAP.items():
92+
if field == "logs":
93+
# Python-only: the Lua mod reads BALATROBOT_LOG_DIR (set
94+
# imperatively by the launcher as the per-instance dir), not
95+
# the parent. Don't emit a confusing second log var.
96+
continue
9297
value = getattr(self, field)
9398
if value is None:
9499
continue

src/balatrobot/instance.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,16 @@ async def start(self) -> None:
110110
session_name = self._session_name or datetime.now().strftime(
111111
"%Y-%m-%dT%H-%M-%S"
112112
)
113-
session_dir = Path(self._config.path_logs or "logs") / session_name
114-
session_dir.mkdir(parents=True, exist_ok=True)
115-
self._log_path = session_dir / f"{self._config.port}.log"
113+
session_dir = Path(self._config.logs or "logs") / session_name
114+
instance_dir = session_dir / str(self._config.port)
115+
instance_dir.mkdir(parents=True, exist_ok=True)
116+
self._log_path = instance_dir / "balatro.log"
116117

117118
# Get launcher and start process
118119
self._launcher = get_launcher(self._config.platform)
119120
print(f"Starting Balatro on port {self._config.port}...")
120121

121-
self._process = await self._launcher.start(self._config, session_dir)
122+
self._process = await self._launcher.start(self._config, instance_dir)
122123

123124
# Wait for health
124125
print(f"Waiting for health check on {self._config.host}:{self._config.port}...")

src/balatrobot/platforms/base.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ def build_cmd(self, config: Config) -> list[str]:
3939
"""
4040
...
4141

42-
async def start(self, config: Config, session_dir: Path) -> subprocess.Popen:
42+
async def start(self, config: Config, instance_dir: Path) -> subprocess.Popen:
4343
"""Start Balatro with the given configuration.
4444
4545
Args:
4646
config: Launcher configuration (mutated with defaults).
47-
session_dir: Directory for log files.
47+
instance_dir: Per-instance directory for log files. Exposed to the
48+
Lua mod as BALATROBOT_LOG_DIR.
4849
4950
Returns:
5051
The subprocess.Popen object.
@@ -54,10 +55,10 @@ async def start(self, config: Config, session_dir: Path) -> subprocess.Popen:
5455
"""
5556
self.validate_paths(config)
5657
env = self.build_env(config)
57-
env["BALATROBOT_PATH_LOGS"] = str(session_dir.resolve())
58+
env["BALATROBOT_LOG_DIR"] = str(instance_dir.resolve())
5859
cmd = self.build_cmd(config)
5960

60-
log_path = session_dir / f"{config.port}.log"
61+
log_path = instance_dir / "balatro.log"
6162

6263
with open(log_path, "w") as log:
6364
process = subprocess.Popen(

src/lua/core/server.lua

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,11 @@ function BB_SERVER.init()
135135

136136
sendInfoMessage("HTTP server listening on http://" .. BB_SERVER.host .. ":" .. BB_SERVER.port, "BB.SERVER")
137137

138-
-- Open JSONL recording files if BALATROBOT_PATH_LOGS is set
139-
local logs_path = os.getenv("BALATROBOT_PATH_LOGS")
138+
-- Open JSONL recording files if BALATROBOT_LOG_DIR is set
139+
local logs_path = os.getenv("BALATROBOT_LOG_DIR")
140140
if logs_path and logs_path ~= "" then
141-
local req_path = logs_path .. "/" .. BB_SERVER.port .. ".req.jsonl"
142-
local res_path = logs_path .. "/" .. BB_SERVER.port .. ".res.jsonl"
141+
local req_path = logs_path .. "/requests.jsonl"
142+
local res_path = logs_path .. "/responses.jsonl"
143143
local rf, rf_err = io.open(req_path, "a")
144144
if rf then
145145
BB_SERVER.req_file = rf

src/lua/utils/screenshot.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ end
6161
--- Encode the current framebuffer to <id>.png (arms ondemand render first).
6262
---@param id integer|string|nil JSON-RPC request id (filename stem)
6363
local function capture_now(id)
64-
local logs = os.getenv("BALATROBOT_PATH_LOGS")
64+
local logs = os.getenv("BALATROBOT_LOG_DIR")
6565
if not logs or logs == "" then
6666
return
6767
end
6868

6969
local safe_id = tostring(id):gsub("[^A-Za-z0-9._-]", "_")
70-
local dir = logs .. "/" .. BB_SETTINGS.port
70+
local dir = logs .. "/screenshots"
7171
if not nativefs.createDirectory(dir) then
7272
sendErrorMessage("Cannot create screenshot dir: " .. dir, "BB.SCREENSHOT")
7373
return

tests/cli/test_config.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_defaults(self, clean_env):
5959
assert config.debug is False
6060
assert config.screenshots is False
6161
assert config.settings is None
62-
assert config.path_logs is None
62+
assert config.logs is None
6363
assert config.path_balatro is None
6464
assert config.path_lovely is None
6565
assert config.path_love is None
@@ -123,14 +123,14 @@ def test_path_fields_use_new_names(self, clean_env, monkeypatch):
123123
monkeypatch.setenv("BALATROBOT_PATH_BALATRO", "/balatro")
124124
monkeypatch.setenv("BALATROBOT_PATH_LOVELY", "/lovely")
125125
monkeypatch.setenv("BALATROBOT_PATH_LOVE", "/love")
126-
monkeypatch.setenv("BALATROBOT_PATH_LOGS", "/logs")
126+
monkeypatch.setenv("BALATROBOT_LOGS", "/logs")
127127

128128
config = Config.from_env()
129129

130130
assert config.path_balatro == "/balatro"
131131
assert config.path_lovely == "/lovely"
132132
assert config.path_love == "/love"
133-
assert config.path_logs == "/logs"
133+
assert config.logs == "/logs"
134134

135135

136136
class TestConfigToEnv:
@@ -179,11 +179,12 @@ def test_uses_new_env_var_names(self):
179179
path_balatro="/balatro",
180180
path_lovely="/lovely",
181181
path_love="/love",
182-
path_logs="/logs",
183182
)
184183
env = config.to_env()
185184

186185
assert env["BALATROBOT_PATH_BALATRO"] == "/balatro"
187186
assert env["BALATROBOT_PATH_LOVELY"] == "/lovely"
188187
assert env["BALATROBOT_PATH_LOVE"] == "/love"
189-
assert env["BALATROBOT_PATH_LOGS"] == "/logs"
188+
# logs is Python-only: not emitted to the subprocess (Lua reads
189+
# BALATROBOT_LOG_DIR, set imperatively by the launcher).
190+
assert "BALATROBOT_LOGS" not in env

tests/cli/test_instance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ async def mock_start(config, session_dir):
164164

165165
monkeypatch.setattr("balatrobot.instance.get_launcher", lambda x: mock_launcher)
166166

167-
instance = BalatroInstance(path_logs=str(tmp_path))
167+
instance = BalatroInstance(logs=str(tmp_path))
168168

169169
# Mock health check to succeed immediately
170170
instance._wait_for_health = AsyncMock() # ty: ignore[invalid-assignment]

0 commit comments

Comments
 (0)