tao-shen Claude Opus 4.6 commited on
Commit
efe59e5
Β·
1 Parent(s): a1676f7

feat: add /logs/stream SSE endpoint for real-time log streaming

Browse files

HF's runtime log API doesn't capture Docker SDK container output
(confirmed: stdout, stderr, /proc/1/fd/*, Python PID 1 all fail).
Added our own SSE streamer at /logs/stream that mimics HF's format:

curl -N https://tao-shen-huggingrun.hf.space/logs/stream

Also available as static file: curl .../logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Dockerfile CHANGED
@@ -47,6 +47,7 @@ RUN mkdir -p /data
47
  COPY ubuntu-server/nginx.conf /etc/nginx/nginx.conf
48
  COPY ubuntu-server/ws-ssh-bridge.py /opt/ws-ssh-bridge.py
49
  COPY ubuntu-server/git_sync_daemon.py /opt/git_sync_daemon.py
 
50
  COPY ubuntu-server/start-server.sh /opt/start-server.sh
51
  COPY scripts /scripts
52
  RUN chmod +x /scripts/entrypoint.sh /scripts/entrypoint_wrapper.py /opt/start-server.sh
@@ -58,6 +59,4 @@ ENV SSH_PORT=2222
58
 
59
  # Run as root (needed for: apt install persistence, bind mounts, sshd)
60
  EXPOSE 7860
61
- # Python PID 1: HF log API captures Python stdout/stderr reliably
62
- # The wrapper tails /var/log/huggingrun.log β†’ stdout/stderr for HF to capture
63
- ENTRYPOINT ["python3", "/scripts/entrypoint_wrapper.py"]
 
47
  COPY ubuntu-server/nginx.conf /etc/nginx/nginx.conf
48
  COPY ubuntu-server/ws-ssh-bridge.py /opt/ws-ssh-bridge.py
49
  COPY ubuntu-server/git_sync_daemon.py /opt/git_sync_daemon.py
50
+ COPY ubuntu-server/log_streamer.py /opt/log_streamer.py
51
  COPY ubuntu-server/start-server.sh /opt/start-server.sh
52
  COPY scripts /scripts
53
  RUN chmod +x /scripts/entrypoint.sh /scripts/entrypoint_wrapper.py /opt/start-server.sh
 
59
 
60
  # Run as root (needed for: apt install persistence, bind mounts, sshd)
61
  EXPOSE 7860
62
+ ENTRYPOINT ["/scripts/entrypoint.sh"]
 
 
scripts/entrypoint_wrapper.py DELETED
@@ -1,84 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Python PID 1 wrapper for HF Spaces log compatibility.
4
- HF's runtime log API captures Python stdout/stderr better than bash.
5
- This wrapper stays as PID 1, runs entrypoint.sh as a child,
6
- and mirrors all log output to stdout/stderr.
7
- """
8
- import subprocess
9
- import sys
10
- import os
11
- import signal
12
- import time
13
- import threading
14
-
15
- LOGFILE = "/var/log/huggingrun.log"
16
-
17
- def log(msg):
18
- """Write to stdout, stderr, and logfile simultaneously."""
19
- line = f"{msg}\n"
20
- sys.stdout.write(line)
21
- sys.stdout.flush()
22
- sys.stderr.write(line)
23
- sys.stderr.flush()
24
- try:
25
- with open(LOGFILE, "a") as f:
26
- f.write(line)
27
- except Exception:
28
- pass
29
-
30
- def tail_logfile():
31
- """Background thread: tail the logfile and print new lines to stdout/stderr."""
32
- # Wait for logfile to exist
33
- while not os.path.exists(LOGFILE):
34
- time.sleep(0.5)
35
-
36
- with open(LOGFILE, "r") as f:
37
- # Skip to current end
38
- f.seek(0, 2)
39
- while True:
40
- line = f.readline()
41
- if line:
42
- line = line.rstrip("\n")
43
- sys.stdout.write(f"{line}\n")
44
- sys.stdout.flush()
45
- sys.stderr.write(f"{line}\n")
46
- sys.stderr.flush()
47
- else:
48
- time.sleep(0.3)
49
-
50
- if __name__ == "__main__":
51
- log("========================================")
52
- log("[wrapper] HuggingRun Python PID 1 wrapper")
53
- log(f"[wrapper] PID={os.getpid()}, Date={time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}")
54
- log(f"[wrapper] Python {sys.version.split()[0]}")
55
- log("========================================")
56
-
57
- # Start background tailer: reads logfile and echos to stdout/stderr
58
- # This ensures all log output (from bash scripts, python daemons) gets
59
- # printed through PID 1's stdout/stderr for HF to capture
60
- tailer = threading.Thread(target=tail_logfile, daemon=True)
61
- tailer.start()
62
-
63
- # Run entrypoint.sh as child process
64
- proc = subprocess.Popen(
65
- ["/bin/bash", "/scripts/entrypoint.sh"],
66
- stdout=subprocess.DEVNULL, # entrypoint writes to logfile
67
- stderr=subprocess.DEVNULL, # entrypoint writes to logfile
68
- )
69
-
70
- # Forward signals to child
71
- def forward_signal(sig, frame):
72
- log(f"[wrapper] Signal {sig} received, forwarding to child PID={proc.pid}")
73
- try:
74
- proc.send_signal(sig)
75
- except ProcessLookupError:
76
- pass
77
-
78
- signal.signal(signal.SIGTERM, forward_signal)
79
- signal.signal(signal.SIGINT, forward_signal)
80
-
81
- # Wait for child to exit
82
- rc = proc.wait()
83
- log(f"[wrapper] Child exited with code {rc}")
84
- sys.exit(rc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ubuntu-server/log_streamer.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ SSE log streamer β€” serves /var/log/huggingrun.log as Server-Sent Events.
4
+ Mimics HF's runtime log API format so you can use the same curl -N pattern.
5
+
6
+ Usage:
7
+ curl -N https://tao-shen-huggingrun.hf.space/logs/stream
8
+ """
9
+ import http.server
10
+ import time
11
+ import os
12
+ import json
13
+ from datetime import datetime, timezone
14
+
15
+ LOGFILE = "/var/log/huggingrun.log"
16
+ PORT = 7863
17
+
18
+
19
+ class SSEHandler(http.server.BaseHTTPRequestHandler):
20
+ def log_message(self, format, *args):
21
+ pass # suppress access logs
22
+
23
+ def do_GET(self):
24
+ if self.path == "/stream":
25
+ self.send_response(200)
26
+ self.send_header("Content-Type", "text/event-stream")
27
+ self.send_header("Cache-Control", "no-cache")
28
+ self.send_header("Connection", "keep-alive")
29
+ self.send_header("Access-Control-Allow-Origin", "*")
30
+ self.end_headers()
31
+
32
+ try:
33
+ # Send existing log content first (history)
34
+ if os.path.exists(LOGFILE):
35
+ with open(LOGFILE, "r") as f:
36
+ for line in f:
37
+ line = line.rstrip("\n")
38
+ if line:
39
+ event = json.dumps({
40
+ "data": line + "\n",
41
+ "timestamp": datetime.now(timezone.utc).isoformat()
42
+ })
43
+ self.wfile.write(f"data: {event}\n\n".encode())
44
+ self.wfile.flush()
45
+
46
+ # Then tail for new lines
47
+ with open(LOGFILE, "r") as f:
48
+ f.seek(0, 2) # end of file
49
+ while True:
50
+ line = f.readline()
51
+ if line:
52
+ line = line.rstrip("\n")
53
+ if line:
54
+ event = json.dumps({
55
+ "data": line + "\n",
56
+ "timestamp": datetime.now(timezone.utc).isoformat()
57
+ })
58
+ self.wfile.write(f"data: {event}\n\n".encode())
59
+ self.wfile.flush()
60
+ else:
61
+ # Send keep-alive comment every 15s
62
+ self.wfile.write(b": keep-alive\n\n")
63
+ self.wfile.flush()
64
+ time.sleep(1)
65
+ except (BrokenPipeError, ConnectionResetError):
66
+ pass
67
+ else:
68
+ self.send_response(404)
69
+ self.end_headers()
70
+
71
+
72
+ if __name__ == "__main__":
73
+ server = http.server.HTTPServer(("127.0.0.1", PORT), SSEHandler)
74
+ server.serve_forever()
ubuntu-server/nginx.conf CHANGED
@@ -33,13 +33,24 @@ http {
33
  proxy_send_timeout 86400;
34
  }
35
 
36
- # /logs β†’ live container logs
37
- location /logs {
38
  default_type text/plain;
39
  add_header Cache-Control "no-cache, no-store";
40
  alias /var/log/huggingrun.log;
41
  }
42
 
 
 
 
 
 
 
 
 
 
 
 
43
  # Everything else β†’ ttyd web terminal (on 7681)
44
  location / {
45
  proxy_pass http://127.0.0.1:7681;
 
33
  proxy_send_timeout 86400;
34
  }
35
 
36
+ # /logs β†’ full log file (static)
37
+ location = /logs {
38
  default_type text/plain;
39
  add_header Cache-Control "no-cache, no-store";
40
  alias /var/log/huggingrun.log;
41
  }
42
 
43
+ # /logs/stream β†’ SSE real-time log stream (like HF's logs/run API)
44
+ location /logs/stream {
45
+ proxy_pass http://127.0.0.1:7863/stream;
46
+ proxy_http_version 1.1;
47
+ proxy_set_header Connection "";
48
+ proxy_buffering off;
49
+ proxy_cache off;
50
+ chunked_transfer_encoding off;
51
+ proxy_read_timeout 86400;
52
+ }
53
+
54
  # Everything else β†’ ttyd web terminal (on 7681)
55
  location / {
56
  proxy_pass http://127.0.0.1:7681;
ubuntu-server/start-server.sh CHANGED
@@ -59,6 +59,17 @@ else
59
  log "[ubuntu] [FAILED] sshd failed to start"
60
  fi
61
 
 
 
 
 
 
 
 
 
 
 
 
62
  # ── WebSocket-to-SSH bridge ───────────────────────────────────────
63
  log "[ubuntu] Starting WS-SSH bridge on 127.0.0.1:7862 ..."
64
  python3 /opt/ws-ssh-bridge.py &
@@ -84,10 +95,11 @@ fi
84
  # ── Process summary ───────────────────────────────────────────────
85
  log "========================================"
86
  log "[ubuntu] Services:"
87
- log "[ubuntu] nginx PID=${NGINX_PID} 0.0.0.0:7860"
88
- log "[ubuntu] sshd PID=${SSHD_PID} 127.0.0.1:${SSH_PORT}"
89
- log "[ubuntu] ws-ssh-bridge PID=${BRIDGE_PID} 127.0.0.1:7862"
90
- log "[ubuntu] ttyd PID=${TTYD_PID} 127.0.0.1:${TTYD_PORT}"
 
91
  log "========================================"
92
  log "[ubuntu] Base packages: $(wc -l < /etc/base-packages.list 2>/dev/null || echo '?')"
93
  log "[ubuntu] Current packages: $(dpkg-query -W -f='\n' 2>/dev/null | wc -l)"
@@ -96,7 +108,8 @@ log "[ubuntu] All processes:"
96
  ps aux --no-headers 2>/dev/null | awk '{printf "[ubuntu] %-8s PID=%-6s %s\n", $1, $2, $11}' | while IFS= read -r line; do log "$line"; done
97
 
98
  log "[ubuntu] ══ System ready ══"
99
- log "[ubuntu] View logs: curl https://<space>.hf.space/logs"
 
100
 
101
  # ── Heartbeat ─────────────────────────────────────────────────────
102
  (while true; do
 
59
  log "[ubuntu] [FAILED] sshd failed to start"
60
  fi
61
 
62
+ # ── SSE log streamer ──────────────────────────────────────────────
63
+ log "[ubuntu] Starting SSE log streamer on 127.0.0.1:7863 ..."
64
+ python3 /opt/log_streamer.py &
65
+ STREAMER_PID=$!
66
+ sleep 1
67
+ if kill -0 $STREAMER_PID 2>/dev/null; then
68
+ log "[ubuntu] [ OK ] SSE log streamer started (PID=${STREAMER_PID})"
69
+ else
70
+ log "[ubuntu] [FAILED] SSE log streamer failed to start"
71
+ fi
72
+
73
  # ── WebSocket-to-SSH bridge ───────────────────────────────────────
74
  log "[ubuntu] Starting WS-SSH bridge on 127.0.0.1:7862 ..."
75
  python3 /opt/ws-ssh-bridge.py &
 
95
  # ── Process summary ───────────────────────────────────────────────
96
  log "========================================"
97
  log "[ubuntu] Services:"
98
+ log "[ubuntu] nginx PID=${NGINX_PID} 0.0.0.0:7860"
99
+ log "[ubuntu] sshd PID=${SSHD_PID} 127.0.0.1:${SSH_PORT}"
100
+ log "[ubuntu] log-streamer PID=${STREAMER_PID} 127.0.0.1:7863"
101
+ log "[ubuntu] ws-ssh-bridge PID=${BRIDGE_PID} 127.0.0.1:7862"
102
+ log "[ubuntu] ttyd PID=${TTYD_PID} 127.0.0.1:${TTYD_PORT}"
103
  log "========================================"
104
  log "[ubuntu] Base packages: $(wc -l < /etc/base-packages.list 2>/dev/null || echo '?')"
105
  log "[ubuntu] Current packages: $(dpkg-query -W -f='\n' 2>/dev/null | wc -l)"
 
108
  ps aux --no-headers 2>/dev/null | awk '{printf "[ubuntu] %-8s PID=%-6s %s\n", $1, $2, $11}' | while IFS= read -r line; do log "$line"; done
109
 
110
  log "[ubuntu] ══ System ready ══"
111
+ log "[ubuntu] View logs: curl https://<space>.hf.space/logs"
112
+ log "[ubuntu] Stream SSE: curl -N https://<space>.hf.space/logs/stream"
113
 
114
  # ── Heartbeat ─────────────────────────────────────────────────────
115
  (while true; do