Local development log sessions¶
Motivation¶
Local debugging previously depended on terminal scrollback or ad-hoc commands
such as tee /tmp/api.log. That made warnings, errors, and pipeline health hard
to review after the terminal was closed, and /tmp logs were outside the
workspace.
User-facing change¶
VS Code local development tasks now mirror stdout/stderr into project-local log
sessions under .logs/local/:
.logs/local/
latest -> 20260515T143012Z-12345
20260515T143012Z-12345/
api.log
worker.log
beat.log
web.log
redis.log
smoke.log
compose-full.log
compose-full-containers.log
The wrapper keeps console output unchanged, so task readiness matchers and
terminal feedback still work. The newest 3 sessions are retained, and each log
chunk is capped at 1 MiB by default. To keep logging from becoming a local
bottleneck, each service log is a bounded 16-chunk ring, file flushes are
batched after the initial header lines, and detached Docker Compose followers
tail only the newest 200 container-log lines before following. High-volume runs
can also set LOCAL_LOG_CONSOLE=false to avoid terminal rendering overhead
while still writing project-local files. Follow-up hardening rejects unsafe
session names, recovers stale log locks, prunes stale chunks above the active
chunk cap, and cleans orphaned detached compose log followers by compose
profile.
Direct terminal launches use the same log path through
scripts/dev/local-run.sh <api|worker|beat|web|redis|smoke|compose-full|compose-local>,
so agents and humans get logs even when they do not start processes through VS
Code tasks. Docker Compose runs get both command logs and, for detached up -d,
a background container-log follower.
API / script diff summary¶
- Added
scripts/dev/run-with-log.sh. - Creates/reuses a fresh local session under
.logs/local/. - Updates
.logs/local/latestto the active session. - Keeps the newest 3 sessions by default.
- Caps each log chunk at
LOCAL_LOG_MAX_BYTES=1048576by default. - Caps each service stream at
LOCAL_LOG_MAX_CHUNKS=16chunks per session. - Flushes the first lines immediately, then batches file flushes with
LOCAL_LOG_FLUSH_LINES=50by default. - Supports
LOCAL_LOG_CONSOLE=falseto disable console mirroring when the terminal itself is the slow path. - Supports
LOCAL_LOG_SESSIONfor a forced shared session. - Rejects unsafe session names before filesystem use.
- Recovers stale lock directories with
LOCAL_LOG_LOCK_STALE_SECONDS=30. - Prunes stale chunk files above the active
LOCAL_LOG_MAX_CHUNKScap. - Added
scripts/dev/local-run.sh. - Provides direct terminal commands for
api,worker,beat,web,redis,smoke,compose-full, andcompose-local. - Sets the same local defaults the VS Code tasks need, then delegates to
run-with-log.sh. - Added
scripts/dev/compose-with-log.sh. - Wraps Docker Compose foreground output into
compose-<profile>.log. - Starts a background
docker compose logs -f --no-colorfollower for detachedup -d, writingcompose-<profile>-containers.log. - Bounds detached history replay with
COMPOSE_LOG_TAIL=200by default. - Stops stale followers for the same compose profile before starting a new
detached follower, and on
down,stop, orrm. - Updated
.vscode/tasks.json. redis: ensure,api: start,worker: start,beat: start,web: dev, andsmoke: apinow run throughlocal-run.sh.- Updated docs.
- README and
scripts/dev/README.mddescribe where to find logs and the retention/chunking rules. - Updated
.gitignore. .logs/is ignored.
Validation evidence¶
$ cd /home/moonchoi/dev/elb-dashboard && bash -n scripts/dev/compose-with-log.sh scripts/dev/local-run.sh scripts/dev/run-with-log.sh
exit=0
$ cd /home/moonchoi/dev/elb-dashboard && python3 -m json.tool .vscode/tasks.json >/dev/null
exit=0
$ cd /home/moonchoi/dev/elb-dashboard && rm -rf .logs/local/validation && LOCAL_LOG_SESSION=validation LOCAL_LOG_MAX_BYTES=1024 scripts/dev/run-with-log.sh api -- bash -lc 'python3 - <<"PY"
print("x" * 1500)
PY' >/tmp/elb-log-smoke.out && find .logs/local/validation -maxdepth 1 -type f -printf '%f %s\n' | sort
api.log 1024
api.log.1 690
$ cd /home/moonchoi/dev/elb-dashboard && rm -rf .logs/local/ring-cap-test && LOCAL_LOG_SESSION=ring-cap-test LOCAL_LOG_MAX_BYTES=1024 LOCAL_LOG_MAX_CHUNKS=2 LOCAL_LOG_FLUSH_LINES=100 scripts/dev/run-with-log.sh api -- bash -lc 'python3 - <<"PY"
for i in range(20):
print(f"line-{i:02d}-" + "x" * 240)
PY' >/tmp/elb-ring-cap.out && find .logs/local/ring-cap-test -maxdepth 1 -type f -printf '%f %s\n' | sort
api.log 1024
api.log.1 150
$ cd /home/moonchoi/dev/elb-dashboard && rm -rf .logs/local/console-off-test && LOCAL_LOG_SESSION=console-off-test LOCAL_LOG_CONSOLE=false scripts/dev/run-with-log.sh api -- bash -lc 'echo hidden-on-console' >/tmp/elb-console-off.out && test ! -s /tmp/elb-console-off.out && grep -q hidden-on-console .logs/local/console-off-test/api.log && wc -c /tmp/elb-console-off.out .logs/local/console-off-test/api.log
0 /tmp/elb-console-off.out
256 .logs/local/console-off-test/api.log
256 total
$ cd /home/moonchoi/dev/elb-dashboard && set +e; LOCAL_LOG_SESSION='../bad' scripts/dev/run-with-log.sh api -- true >/tmp/elb-bad-session.out 2>&1; rc=$?; set -e; test "$rc" -eq 2 && grep -q unsafe /tmp/elb-bad-session.out
exit=0; unsafe session names are rejected.
$ cd /home/moonchoi/dev/elb-dashboard && rm -rf .logs/local/stale-lock-test .logs/local/.lock && mkdir -p .logs/local/.lock && touch -d '2 minutes ago' .logs/local/.lock && LOCAL_LOG_SESSION=stale-lock-test LOCAL_LOG_LOCK_STALE_SECONDS=1 scripts/dev/run-with-log.sh api -- true >/tmp/elb-stale-lock.out && test -s .logs/local/stale-lock-test/api.log && test ! -d .logs/local/.lock
exit=0; stale lock recovered.
$ cd /home/moonchoi/dev/elb-dashboard && rm -rf .logs/local/stale-chunk-test && mkdir -p .logs/local/stale-chunk-test && : > .logs/local/stale-chunk-test/api.log.99 && LOCAL_LOG_SESSION=stale-chunk-test LOCAL_LOG_MAX_CHUNKS=2 scripts/dev/run-with-log.sh api -- true >/tmp/elb-stale-chunk.out && test ! -e .logs/local/stale-chunk-test/api.log.99
exit=0; stale chunks above the active cap are removed.
$ cd /home/moonchoi/dev/elb-dashboard && rm -rf .logs/local/direct-api-help && LOCAL_LOG_SESSION=direct-api-help scripts/dev/local-run.sh api -- --help >/tmp/elb-local-run-api-help.out && test -s .logs/local/direct-api-help/api.log && grep -q 'Usage: uvicorn' .logs/local/direct-api-help/api.log
exit=0; direct terminal api launch wrote .logs/local/direct-api-help/api.log
$ cd /home/moonchoi/dev/elb-dashboard && LOCAL_LOG_SESSION=compose-config-test scripts/dev/local-run.sh compose-full -- config --services >/tmp/elb-compose-config.out && test -s .logs/local/compose-config-test/compose-full.log && grep -q '^api$' /tmp/elb-compose-config.out
exit=0; compose command output wrote compose-full.log
$ cd /home/moonchoi/dev/elb-dashboard && rm -rf .logs/local/compose-detached-test && LOCAL_LOG_SESSION=compose-detached-test scripts/dev/local-run.sh compose-full -- up -d --no-build terminal >/tmp/elb-compose-detached.out && test -s .logs/local/compose-detached-test/compose-full.log && test -s .logs/local/compose-detached-test/compose-full-containers.log && pid=$(cat .logs/local/.compose-full-containers.pid); kill "$pid" 2>/dev/null || true; rm -f .logs/local/.compose-full-containers.pid; find .logs/local/compose-detached-test -maxdepth 1 -type f -printf '%f %s\n' | sort
compose-full-containers.log 333
compose-full.log 378
$ cd /home/moonchoi/dev/elb-dashboard && rm -rf .logs/local/compose-tail-test && COMPOSE_LOG_TAIL=5 LOCAL_LOG_SESSION=compose-tail-test scripts/dev/local-run.sh compose-full -- up -d --no-build terminal >/tmp/elb-compose-tail.out && test -s .logs/local/compose-tail-test/compose-full-containers.log && grep -q -- '--tail 5' .logs/local/compose-tail-test/compose-full-containers.log; pid=$(cat .logs/local/.compose-full-containers.pid); kill "$pid" 2>/dev/null || true; rm -f .logs/local/.compose-full-containers.pid
exit=0; detached compose follower used bounded replay and wrote an immediate header.
$ cd /home/moonchoi/dev/elb-dashboard && LOCAL_LOG_SESSION=follower-cleanup-test COMPOSE_LOG_TAIL=5 scripts/dev/local-run.sh compose-full -- up -d --no-build terminal >/tmp/elb-follower-cleanup.out && pid=$(cat .logs/local/.compose-full-containers.pid); test -n "$pid" && kill -0 "$pid" && kill -TERM -- "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true; rm -f .logs/local/.compose-full-containers.pid; ps -ef | grep -E 'docker compose -p elb-control-local .* logs -f|run-with-log.sh compose-full-containers' | grep -v grep || true
exit=0; stale compose log followers were cleaned up.
$ cd /home/moonchoi/dev/elb-dashboard && LOCAL_LOG_SESSION=retention-check scripts/dev/run-with-log.sh smoke -- true >/tmp/elb-log-retention.out
exit=0; retention cleanup kept the newest 3 session directories.