Skip to content

Commit 31347b9

Browse files
feat: v3 dashboard with live request feed and white-on-dark style
- Add RequestLog ring buffer recording every S3 proxy request - Hook into request_handler.py finally block (method, path, op, status, latency, size) - Live Feed table: scrolling last 50 requests with color-coded methods, statuses, latency warnings, and ENC/DEC crypto badges - Replace verbose pod cards with compact inline badges in header - Remove redundant Bandwidth and Active Uploads sections - Switch from green-on-dark to white-on-dark (#f0f6fc on #0f1117) modern style - New row fade-in animation for realtime feel
1 parent eb6b847 commit 31347b9

File tree

4 files changed

+260
-139
lines changed

4 files changed

+260
-139
lines changed

s3proxy/admin/collectors.py

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import os
88
import time
99
from collections import deque
10-
from datetime import UTC, datetime
10+
from dataclasses import asdict, dataclass
1111
from typing import TYPE_CHECKING
1212

1313
import structlog
@@ -77,6 +77,84 @@ def history(self, key: str, max_points: int = 60) -> list[float]:
7777
_rate_tracker = RateTracker(window_seconds=600)
7878

7979

80+
# ---------------------------------------------------------------------------
81+
# Request log — ring buffer for live feed
82+
# ---------------------------------------------------------------------------
83+
84+
85+
@dataclass(slots=True, frozen=True)
86+
class RequestEntry:
87+
"""Single request log entry for the live feed."""
88+
89+
timestamp: float
90+
method: str
91+
path: str
92+
operation: str
93+
status: int
94+
duration_ms: float
95+
size: int
96+
crypto: str
97+
98+
99+
class RequestLog:
100+
"""Fixed-size ring buffer of recent requests for the live feed."""
101+
102+
ENCRYPT_OPS = frozenset({
103+
"PutObject", "UploadPart", "UploadPartCopy",
104+
"CompleteMultipartUpload", "CopyObject",
105+
})
106+
DECRYPT_OPS = frozenset({"GetObject"})
107+
108+
def __init__(self, maxlen: int = 200):
109+
self._entries: deque[RequestEntry] = deque(maxlen=maxlen)
110+
111+
def record(
112+
self,
113+
method: str,
114+
path: str,
115+
operation: str,
116+
status: int,
117+
duration: float,
118+
size: int,
119+
) -> None:
120+
crypto = ""
121+
if operation in self.ENCRYPT_OPS:
122+
crypto = "encrypt"
123+
elif operation in self.DECRYPT_OPS:
124+
crypto = "decrypt"
125+
self._entries.append(RequestEntry(
126+
timestamp=time.time(),
127+
method=method,
128+
path=path[:120],
129+
operation=operation,
130+
status=status,
131+
duration_ms=round(duration * 1000, 1),
132+
size=size,
133+
crypto=crypto,
134+
))
135+
136+
def recent(self, limit: int = 50) -> list[dict]:
137+
"""Return most recent entries as dicts, newest first."""
138+
entries = list(self._entries)
139+
entries.reverse()
140+
return [asdict(e) for e in entries[:limit]]
141+
142+
143+
_request_log = RequestLog(maxlen=200)
144+
145+
146+
def record_request(
147+
method: str,
148+
path: str,
149+
operation: str,
150+
status: int,
151+
duration: float,
152+
size: int,
153+
) -> None:
154+
"""Record a completed request to the live feed log."""
155+
_request_log.record(method, path, operation, status, duration, size)
156+
157+
80158
# ---------------------------------------------------------------------------
81159
# Prometheus helpers
82160
# ---------------------------------------------------------------------------
@@ -206,23 +284,6 @@ def collect_throughput() -> dict:
206284
}
207285

208286

209-
async def collect_upload_status(handler: S3ProxyHandler) -> dict:
210-
"""Collect active multipart upload status with stale detection."""
211-
uploads = await handler.multipart_manager.list_active_uploads()
212-
now = datetime.now(UTC)
213-
for upload in uploads:
214-
created = datetime.fromisoformat(upload["created_at"])
215-
if created.tzinfo is None:
216-
created = created.replace(tzinfo=UTC)
217-
age_seconds = (now - created).total_seconds()
218-
upload["is_stale"] = age_seconds > 1800
219-
upload["total_plaintext_size_formatted"] = _format_bytes(upload["total_plaintext_size"])
220-
return {
221-
"active_count": len(uploads),
222-
"uploads": uploads,
223-
}
224-
225-
226287
# ---------------------------------------------------------------------------
227288
# Redis pod metrics publishing (multi-pod view)
228289
# ---------------------------------------------------------------------------
@@ -301,7 +362,6 @@ async def collect_all(
301362
pod = collect_pod_identity(settings, start_time)
302363
health = collect_health()
303364
throughput = collect_throughput()
304-
upload_status = await collect_upload_status(handler)
305365

306366
local_data = {
307367
"pod": pod,
@@ -328,6 +388,6 @@ async def collect_all(
328388

329389
return {
330390
**local_data,
331-
"uploads": upload_status,
391+
"request_log": _request_log.recent(50),
332392
"all_pods": all_pods,
333393
}

0 commit comments

Comments
 (0)