Skip to content

Commit 15a4696

Browse files
committed
Add build history and server logs API endpoints
- /api/v1/builds/history: returns last 50 builds with status, duration, built/cached counts, sessions processed, and errors - /api/v1/logs: tails the JSON log file with optional level and event filters (limit, level, event query params)
1 parent ab05a31 commit 15a4696

File tree

1 file changed

+61
-0
lines changed

1 file changed

+61
-0
lines changed

src/synix/mesh/server.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def create_app(config: MeshConfig) -> Starlette:
4949
_build_task: asyncio.Task | None = None # tracked to prevent GC
5050
_state_lock = asyncio.Lock()
5151

52+
# Build history ring buffer — last 50 builds
53+
from collections import deque
54+
build_history: deque[dict] = deque(maxlen=50)
55+
5256
# --- Term fencing helper (1C) ---
5357
async def _check_term(request: Request) -> JSONResponse | None:
5458
"""Check term from request headers. Returns 409 response if stale, None if OK."""
@@ -419,6 +423,44 @@ async def session_file(request: Request) -> Response:
419423
},
420424
)
421425

426+
async def builds_history(request: Request) -> JSONResponse:
427+
"""Return build history (last 50 builds, newest first)."""
428+
return JSONResponse({"builds": list(reversed(build_history))})
429+
430+
async def logs_tail(request: Request) -> JSONResponse:
431+
"""Return recent structured log entries from the JSON log file."""
432+
limit = min(int(request.query_params.get("limit", "100")), 500)
433+
level_filter = request.query_params.get("level", "").upper()
434+
event_filter = request.query_params.get("event", "")
435+
436+
log_path = mesh_dir / "logs" / "server.log"
437+
if not log_path.exists():
438+
return JSONResponse({"entries": [], "log_file": str(log_path)})
439+
440+
# Read last N lines efficiently (read from end)
441+
entries: list[dict] = []
442+
try:
443+
raw_lines = log_path.read_text(encoding="utf-8", errors="replace").splitlines()
444+
for line in reversed(raw_lines):
445+
if len(entries) >= limit:
446+
break
447+
line = line.strip()
448+
if not line:
449+
continue
450+
try:
451+
entry = json.loads(line)
452+
except json.JSONDecodeError:
453+
continue
454+
if level_filter and entry.get("level", "") != level_filter:
455+
continue
456+
if event_filter and entry.get("event", "") != event_filter:
457+
continue
458+
entries.append(entry)
459+
except OSError as exc:
460+
return JSONResponse({"error": f"Cannot read log: {exc}"}, status_code=500)
461+
462+
return JSONResponse({"entries": entries, "count": len(entries)})
463+
422464
async def _run_build() -> None:
423465
nonlocal build_count, current_bundle_etag, current_bundle_path
424466

@@ -479,6 +521,15 @@ async def _run_build() -> None:
479521
"cached": result.cached,
480522
},
481523
)
524+
build_history.append({
525+
"build_number": local_build_count,
526+
"status": "success",
527+
"started_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(build_start)),
528+
"duration": round(build_duration, 1),
529+
"built": result.built,
530+
"cached": result.cached,
531+
"sessions_processed": len(pre_build_unprocessed),
532+
})
482533

483534
# 4. Run server deploy hooks
484535
if config.deploy.server_commands:
@@ -570,6 +621,14 @@ async def _run_build() -> None:
570621

571622
except Exception as exc:
572623
mesh_event(logger, logging.ERROR, f"Build failed: {exc}", "build_failed", {"error": str(exc)})
624+
build_history.append({
625+
"build_number": build_count + 1,
626+
"status": "failed",
627+
"started_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(build_start)),
628+
"duration": round(time.time() - build_start, 1),
629+
"error": str(exc),
630+
"sessions_processed": len(pre_build_unprocessed),
631+
})
573632

574633
# 9. Complete scheduler and check if another build is needed
575634
needs_another = await scheduler.complete_build()
@@ -608,7 +667,9 @@ async def lifespan(app: Starlette):
608667
Route("/api/v1/sessions/manifest", sessions_manifest),
609668
Route("/api/v1/sessions/{session_id}/file", session_file),
610669
Route("/api/v1/builds/status", build_status),
670+
Route("/api/v1/builds/history", builds_history),
611671
Route("/api/v1/builds/trigger", trigger_build, methods=["POST"]),
672+
Route("/api/v1/logs", logs_tail),
612673
Route("/api/v1/artifacts/manifest", artifact_manifest),
613674
Route("/api/v1/artifacts/bundle", artifact_bundle),
614675
Route("/api/v1/search", search, methods=["POST"]),

0 commit comments

Comments
 (0)