Show recent logs
This commit is contained in:
32
app.py
32
app.py
@@ -125,6 +125,37 @@ def docker_inspect_restart_count(container_name: str) -> int:
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def get_container_logs(container_name: str, lines: int = 50) -> list[dict]:
|
||||
"""
|
||||
Get last N lines of container logs with error detection.
|
||||
Returns list of dicts with 'text' and 'level' keys.
|
||||
"""
|
||||
try:
|
||||
out = sh([DOCKER, "logs", "--tail", str(lines), container_name])
|
||||
log_lines = []
|
||||
|
||||
for line in out.splitlines():
|
||||
# Strip ANSI color codes
|
||||
line_clean = re.sub(r'\x1b\[[0-9;]*m', '', line)
|
||||
|
||||
# Detect log level
|
||||
line_lower = line_clean.lower()
|
||||
if any(x in line_lower for x in ['error', 'exception', 'fatal', 'critical']):
|
||||
level = 'error'
|
||||
elif any(x in line_lower for x in ['warn', 'warning']):
|
||||
level = 'warn'
|
||||
else:
|
||||
level = 'info'
|
||||
|
||||
log_lines.append({
|
||||
'text': line_clean,
|
||||
'level': level
|
||||
})
|
||||
|
||||
return log_lines
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def is_app_web_container(name: str) -> bool:
|
||||
# Dokku apps typically have containers like "<app>.web.1"
|
||||
return name.endswith(".web.1") and not name.startswith("dokku.")
|
||||
@@ -169,6 +200,7 @@ def collect():
|
||||
"mem_limit": s.get("mem_limit", ""),
|
||||
"mem_pct": s.get("mem_pct", ""),
|
||||
"restarts": docker_inspect_restart_count(name),
|
||||
"logs": get_container_logs(name, lines=50),
|
||||
}
|
||||
|
||||
if is_app_web_container(name):
|
||||
|
||||
@@ -366,4 +366,102 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- APPLICATION LOGS SECTION -->
|
||||
<h2 style="margin-top: 32px;">[ APPLICATION LOGS ]</h2>
|
||||
{% for r in data.apps %}
|
||||
<div style="
|
||||
border: 2px solid #30363d;
|
||||
margin-bottom: 16px;
|
||||
background: rgba(0, 217, 255, 0.02);
|
||||
">
|
||||
<!-- Header -->
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
" onclick="toggleLogs('logs-{{ loop.index }}')">
|
||||
<div style="font-weight: 700; color: #00d9ff;">[{{ r.app }}]</div>
|
||||
<button style="
|
||||
background: rgba(0, 217, 255, 0.1);
|
||||
border: 1px solid #00d9ff;
|
||||
color: #00d9ff;
|
||||
padding: 4px 12px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
">[EXPAND]</button>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible logs -->
|
||||
<div id="logs-{{ loop.index }}" style="display: none; border-top: 2px solid #30363d;">
|
||||
<div style="padding: 16px; background: #0a0e17;">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 12px;">
|
||||
<div style="color: #00d9ff; font-size: 10px; font-weight: 700; letter-spacing: 2px;">
|
||||
[LAST 50 LINES]
|
||||
</div>
|
||||
<button onclick="event.stopPropagation(); copyLogs('logs-content-{{ loop.index }}')" style="
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border: 1px solid #00ff88;
|
||||
color: #00ff88;
|
||||
padding: 4px 10px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
" onmouseover="this.style.background='rgba(0, 255, 136, 0.2)';"
|
||||
onmouseout="this.style.background='rgba(0, 255, 136, 0.1)';">
|
||||
COPY
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="logs-content-{{ loop.index }}" style="
|
||||
background: #000;
|
||||
border: 1px solid #30363d;
|
||||
padding: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
">
|
||||
{% if r.logs %}
|
||||
{% for log in r.logs %}
|
||||
<div
|
||||
style="color: {% if log.level == 'error' %}#ff5555{% elif log.level == 'warn' %}#ffb86c{% else %}#8b949e{% endif %};">
|
||||
{{ log.text }}</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div style="color: #8b949e;">[no logs available]</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<script>
|
||||
function toggleLogs(id) {
|
||||
const el = document.getElementById(id);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function copyLogs(id) {
|
||||
const el = document.getElementById(id);
|
||||
navigator.clipboard.writeText(el.innerText).then(() => {
|
||||
const btn = event.target;
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = 'COPIED!';
|
||||
btn.style.background = 'rgba(0, 255, 136, 0.3)';
|
||||
setTimeout(() => {
|
||||
btn.textContent = orig;
|
||||
btn.style.background = 'rgba(0, 255, 136, 0.1)';
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user