Host CPU, memory and load per core, container CPU and memory, and every monitor's response time learn a normal per hour (milestone 513, System 182). A reading past its fixed cutoff but within its normal reads healthy and says what is usual at this hour; one well out of it keeps its grade and is a metric anomaly. Disk, a monitor down and an agent offline keep their cutoffs. - Each normal's floor, the smallest departure that counts, is a setting: Settings -> Thresholds -> Normals (norms.floor.*). Defaults: host CPU 10 points, host memory 5, load per core 10, container CPU 10 points of one core, container memory 5, monitor response 100 ms; Traefik's 2-point 4xx and 50 ms p95 floors move there from constants. learn_normal takes a floor function (floor_setting) read at each pass. - learn_normal(parts=False): a host learns, its cores don't. - Ingest records load_pct (load_1m / cores x 100), the figure pages grade. - Shared grading: anomalies.against / graded, worst_reading(normals=), hosts' host_grades, docker usage(thresholds=, norms=), monitors' latency_grade. Applied to host vitals, the Hosts widget, MCP overview, Docker usage, and the monitor list, widget and page. - The monitor and container pages list their anomalies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
7.1 KiB
HTML
130 lines
7.1 KiB
HTML
{# docker/container_detail.html — one container (#5310), shaped like a
|
|
service's page: a header that says whether it is well and what it is; why
|
|
it stopped, only while it has; what it moved since it started; its CPU and
|
|
memory over time (the one home for what it uses now), with its metric
|
|
anomalies above when it has any; its lifecycle.
|
|
A swarm task is titled by its service and slot, the raw name under it. #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs, status_badge %}
|
|
{% block title %}{{ label or name }} — Docker — Steward{% endblock %}
|
|
{% block breadcrumb %}
|
|
{%- if container is not none and container.service_name -%}
|
|
{{ crumbs([("Docker", "/plugins/docker/"), (container.service_name, "/plugins/docker/service/" ~ container.service_name), (label, "")]) }}
|
|
{%- else -%}
|
|
{{ crumbs([("Docker", "/plugins/docker/"), (label or name, "")]) }}
|
|
{%- endif -%}
|
|
{% endblock %}
|
|
{% block content %}
|
|
|
|
{% if container is none %}
|
|
<div class="card empty-state">
|
|
<strong>Container not found</strong>
|
|
<span>No container named <code>{{ name }}</code> is reported for this host now. It may have been
|
|
removed, or the host agent has not reported recently. <a href="/plugins/docker/">Back to Docker</a></span>
|
|
</div>
|
|
{% else %}
|
|
{% set c = container %}
|
|
|
|
{# ── Header: the verdict, then what it is ────────────────────────────────── #}
|
|
<div class="page-header">
|
|
<div style="min-width:0;">
|
|
<h1 class="page-title" style="overflow-wrap:anywhere;">{{ label }}</h1>
|
|
<p class="page-subtitle">
|
|
{% if c.status == 'running' %}{{ status_badge('ok', 'Running') }}
|
|
{% elif c.status == 'restarting' %}{{ status_badge('crit', 'Restarting') }}
|
|
{% elif c.status == 'paused' %}{{ status_badge('warn', 'Paused') }}
|
|
{% elif exit_failed %}{{ status_badge('crit', c.status|capitalize) }}
|
|
{% else %}{{ status_badge('dim', c.status|capitalize) }}{% endif %}
|
|
{%- if c.health == 'unhealthy' %} {{ status_badge('crit', 'Unhealthy') }}
|
|
{%- elif c.health == 'starting' %} {{ status_badge('pending', 'Starting') }}
|
|
{%- elif c.health == 'healthy' %} {{ status_badge('ok', 'Healthy') }}{% endif %}
|
|
{%- if uptime %} · up {{ uptime }}{% endif %}
|
|
{%- if host %} · on <a href="/hosts/{{ host.id }}">{{ host.name }}</a>{% endif %}
|
|
{%- if c.service_name %} · task of <a href="/plugins/docker/service/{{ c.service_name }}">{{ c.service_name }}</a>
|
|
{%- elif c.compose_project %} · compose <span class="chip">{{ c.compose_project }}</span>{% endif %}
|
|
{%- if c.image %} · <span class="mono" style="overflow-wrap:anywhere;" title="{{ c.image }}">{{ c.image.split("@")[0] }}</span>{% endif %}
|
|
</p>
|
|
<p class="page-subtitle tone-dim" style="margin-top:0.2rem;">
|
|
{%- if label != c.name %}<span class="mono" style="overflow-wrap:anywhere;">{{ c.name }}</span> · {% endif -%}
|
|
{%- if c.scraped_at %}reported {{ c.scraped_at|ago }}{% endif -%}
|
|
</p>
|
|
</div>
|
|
<div class="page-actions"><a class="btn btn-sm btn-ghost" href="/plugins/docker/container/{{ host_id }}/{{ name }}/logs">Logs</a></div>
|
|
</div>
|
|
|
|
{# ── Why it stopped, only while it has ───────────────────────────────────── #}
|
|
{% if c.status != 'running' and exit_failed %}
|
|
<section class="card problem-card" aria-labelledby="ctr-exit">
|
|
<h2 id="ctr-exit" class="section-title" style="margin-bottom:0.4rem;">Last exit</h2>
|
|
<p style="margin:0;">
|
|
Exited <span class="tone-crit">{{ c.exit_code }}</span>{% if c.oom_killed %}: killed for running out of memory{% endif %}
|
|
{%- if died_at %} · {{ died_at|ago }}{% endif %}
|
|
· <a href="/plugins/docker/container/{{ host_id }}/{{ name }}/logs">its logs →</a>
|
|
</p>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# ── Its CPU or memory out of its normal for the hour, and past anomalies (#5742) #}
|
|
{% with back=anomaly_back, base_resource=anomaly_resource %}{% include "_anomaly_card.html" %}{% endwith %}
|
|
|
|
{# ── What it is set up with, and what it moved since it started ─────────── #}
|
|
<div class="card">
|
|
<dl class="kv">
|
|
{% if not c.service_name %}
|
|
{#- Swarm never restarts a task's container: it replaces the task, so a
|
|
task's restart count is always 0; its service's Tasks table is the
|
|
count that means something. #}
|
|
<dt>Restarts</dt><dd>{% if c.restart_count %}<span class="tone-warn">{{ c.restart_count }}</span>{% else %}0{% endif %}
|
|
{%- if c.exit_code is not none and c.status == 'running' and c.restart_count %} <span class="tone-dim">· last exit {{ c.exit_code }}</span>{% endif %}</dd>
|
|
{% endif %}
|
|
<dt>Memory limit</dt><dd>{% if usage.mem_limit %}{{ usage.mem_limit|bytes }}{% else %}<span class="tone-dim">none set</span>{% endif %}</dd>
|
|
{% if ports %}
|
|
<dt>Ports</dt><dd class="mono">{% for p in ports %}{{ p.host_port }}:{{ p.container_port }}/{{ p.protocol }}{% if not loop.last %}, {% endif %}{% endfor %}</dd>
|
|
{% endif %}
|
|
{% if c.node_id %}
|
|
<dt>Swarm node</dt><dd title="node id {{ c.node_id }}">{{ node_name or c.node_id[:12] }}</dd>
|
|
{% endif %}
|
|
<dt>Network</dt><dd>{{ net_rx }} in · {{ net_tx }} out <span class="tone-dim">since it started</span></dd>
|
|
<dt>Disk</dt><dd>{{ blk_read }} read · {{ blk_write }} written <span class="tone-dim">since it started</span></dd>
|
|
</dl>
|
|
</div>
|
|
|
|
{# ── Resource history: the one home for what it uses ────────────────────── #}
|
|
<section class="card" aria-labelledby="ctr-history">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:0.75rem;flex-wrap:wrap;">
|
|
<h2 id="ctr-history" class="section-title" style="margin:0;">Resource history</h2>
|
|
{% include "_time_range.html" %}
|
|
</div>
|
|
<div id="container-history"
|
|
hx-get="/plugins/docker/container/{{ host_id }}/{{ name }}/history"
|
|
hx-trigger="load, rangeChange from:body"
|
|
hx-include="#time-range"
|
|
hx-swap="innerHTML">
|
|
<p class="field-hint">Loading…</p>
|
|
</div>
|
|
</section>
|
|
|
|
{# ── Lifecycle, newest first ─────────────────────────────────────────────── #}
|
|
<section class="card-flush" aria-labelledby="ctr-lifecycle">
|
|
<h2 id="ctr-lifecycle" class="table-caption">Lifecycle <span class="tone-muted">newest first</span></h2>
|
|
{% if events %}
|
|
<table class="table">
|
|
<thead><tr><th>Event</th><th>Detail</th><th class="num">When</th></tr></thead>
|
|
<tbody>
|
|
{% for e in events %}
|
|
<tr>
|
|
<td><span class="tone-{{ e.tone }}" aria-hidden="true">{{ e.glyph }}</span> {{ e.event|replace("_", " ") }}</td>
|
|
<td class="tone-muted" style="overflow-wrap:anywhere;">{{ e.detail or "" }}</td>
|
|
<td class="num">{{ e.at|ago }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="tone-muted" style="margin:0;padding:1rem;">No lifecycle events recorded yet. Starts, stops and health changes appear here as the agent reports them.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{% endif %}
|
|
{% endblock %}
|