Skip to content

Commit 502e711

Browse files
Merge pull request #806 from yasinBursali/feat/service-logs-agent
feat(host-agent): add read-only service logs endpoint for all services
2 parents 127a27c + 9a1ec1c commit 502e711

File tree

1 file changed

+76
-0
lines changed

1 file changed

+76
-0
lines changed

dream-server/bin/dream-host-agent.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,27 @@ def validate_service_id(handler, body: dict) -> str | None:
228228
return sid
229229

230230

231+
def _resolve_container_name(service_id: str) -> str:
232+
"""Resolve actual container name via Docker Compose labels.
233+
234+
Falls back to dream-{service_id} convention if label lookup fails.
235+
"""
236+
try:
237+
result = subprocess.run(
238+
["docker", "ps", "--filter",
239+
f"label=com.docker.compose.service={service_id}",
240+
"--filter", "label=com.docker.compose.project=dream-server",
241+
"--format", "{{.Names}}"],
242+
capture_output=True, text=True, timeout=5,
243+
)
244+
names = result.stdout.strip().splitlines()
245+
if names:
246+
return names[0]
247+
except (subprocess.TimeoutExpired, OSError):
248+
pass
249+
return f"dream-{service_id}"
250+
251+
231252
class AgentHandler(BaseHTTPRequestHandler):
232253
def log_message(self, fmt, *args):
233254
logger.info(fmt, *args)
@@ -317,6 +338,8 @@ def do_POST(self):
317338
self._handle_logs()
318339
elif self.path == "/v1/extension/setup-hook":
319340
self._handle_setup_hook()
341+
elif self.path == "/v1/service/logs":
342+
self._handle_service_logs()
320343
else:
321344
json_response(self, 404, {"error": "Not found"})
322345

@@ -391,6 +414,59 @@ def _handle_logs(self):
391414
json_response(self, 500, {"error": f"Failed to fetch logs: {exc}"})
392415

393416

417+
def _handle_service_logs(self):
418+
"""Read-only log access for ANY service (core + extensions).
419+
420+
Unlike _handle_logs() which uses validate_service_id() and blocks
421+
core services, this endpoint only validates the service_id format.
422+
"""
423+
if not check_auth(self):
424+
return
425+
body = read_json_body(self)
426+
if body is None:
427+
return
428+
429+
sid = body.get("service_id", "")
430+
if not isinstance(sid, str) or not SERVICE_ID_RE.match(sid):
431+
json_response(self, 400, {"error": "Invalid service_id"})
432+
return
433+
434+
try:
435+
tail = min(max(int(body.get("tail", 100)), 1), 500)
436+
except (ValueError, TypeError):
437+
tail = 100
438+
439+
container_name = _resolve_container_name(sid)
440+
441+
try:
442+
result = subprocess.run(
443+
["docker", "logs", "--tail", str(tail), container_name],
444+
capture_output=True, text=True, timeout=5,
445+
)
446+
if result.returncode != 0 and "no such container" in (result.stderr or "").lower():
447+
json_response(self, 200, {
448+
"service_id": sid,
449+
"container_name": container_name,
450+
"logs": "Container is not running.",
451+
"lines": 0,
452+
})
453+
return
454+
if result.returncode != 0:
455+
json_response(self, 500, {"error": f"docker logs failed: {(result.stderr or '')[:500]}"})
456+
return
457+
output = result.stdout or result.stderr or ""
458+
json_response(self, 200, {
459+
"service_id": sid,
460+
"container_name": container_name,
461+
"logs": output[-50000:],
462+
"lines": tail,
463+
})
464+
except subprocess.TimeoutExpired:
465+
json_response(self, 503, {"error": "Log fetch timed out"})
466+
except Exception as exc:
467+
json_response(self, 500, {"error": f"Failed to fetch logs: {exc}"})
468+
469+
394470
def _handle_setup_hook(self):
395471
if not check_auth(self):
396472
return

0 commit comments

Comments
 (0)