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:
|
except Exception:
|
||||||
return 0
|
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:
|
def is_app_web_container(name: str) -> bool:
|
||||||
# Dokku apps typically have containers like "<app>.web.1"
|
# Dokku apps typically have containers like "<app>.web.1"
|
||||||
return name.endswith(".web.1") and not name.startswith("dokku.")
|
return name.endswith(".web.1") and not name.startswith("dokku.")
|
||||||
@@ -169,6 +200,7 @@ def collect():
|
|||||||
"mem_limit": s.get("mem_limit", ""),
|
"mem_limit": s.get("mem_limit", ""),
|
||||||
"mem_pct": s.get("mem_pct", ""),
|
"mem_pct": s.get("mem_pct", ""),
|
||||||
"restarts": docker_inspect_restart_count(name),
|
"restarts": docker_inspect_restart_count(name),
|
||||||
|
"logs": get_container_logs(name, lines=50),
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_app_web_container(name):
|
if is_app_web_container(name):
|
||||||
|
|||||||
@@ -367,3 +367,101 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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