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>
52 lines
3.6 KiB
HTML
52 lines
3.6 KiB
HTML
{# docker/widget_resources.html — the dashboard Busiest Containers widget
|
|
(#5253, #5214): a verdict naming the busiest, then the top containers by CPU
|
|
(averaged over the last few minutes, so the order holds still). Each figure
|
|
carries its unit, beside bars of the last few hours. A row's bars are scaled
|
|
to its own peak, but never below a floor (1 core, 1 GiB by default), so a
|
|
container idling at 0.05 cores reads flat and a spike means real load. #}
|
|
{% macro cores(v) -%}{{ "%.2f"|format(v) if v < 1 else "%.1f"|format(v) }}{%- endmacro %}
|
|
{% macro bars(b, peak_text, roof_text) -%}
|
|
<span class="w-bars w-wide-only" aria-hidden="true"
|
|
title="{% if b.peak is none %}No samples in the last {{ hours }}h{% else %}Peak {{ peak_text }} in the last {{ hours }}h; bars reach the top at {{ roof_text }}{% endif %}">
|
|
{%- for h in b.bars %}<i{% if h is none %} class="gap"{% else %} style="height:{{ [h, 4]|max }}%"{% endif %}></i>{% endfor -%}
|
|
</span>
|
|
{%- endmacro %}
|
|
{% if not rows %}
|
|
<div class="w-verdict" style="color:var(--text-muted);">No running containers.</div>
|
|
{% else %}
|
|
{% set top = rows[0] %}
|
|
<div class="w-verdict">
|
|
Busiest: <strong>{{ top.label }}</strong> on {{ top.host_name }},
|
|
{% if top.usage.cores is not none %}{{ cores(top.usage.cores) }}
|
|
{%- if top.usage.cpus %} of {{ top.usage.cpus }} cores{% else %} cores{% endif %}{% else %}no CPU reading yet{% endif %}.
|
|
</div>
|
|
<table class="w-table">
|
|
<colgroup><col><col class="w-wide-only" style="width:7.5rem"><col class="w-barcol"><col class="w-barcol"></colgroup>
|
|
<thead><tr><th>Container</th><th class="w-wide-only w-text">Host</th>
|
|
<th title="Cores, averaged over the last {{ minutes }} minutes; bars: the last {{ hours }}h">CPU<span class="w-wide-only"> · last {{ hours }}h</span></th>
|
|
<th title="In use now; bars: the last {{ hours }}h">Memory<span class="w-wide-only"> · last {{ hours }}h</span></th></tr></thead>
|
|
<tbody>
|
|
{% for r in rows %}
|
|
{% set u = r.usage %}
|
|
<tr>
|
|
<td class="w-name" title="{{ r.c.name }}">
|
|
{%- if session.user_id %}<a href="/plugins/docker/container/{{ r.c.host_id }}/{{ r.c.name }}">{{ r.label }}</a>{% else %}{{ r.label }}{% endif -%}
|
|
</td>
|
|
<td class="w-wide-only w-text" title="{{ r.host_name }}">{{ r.host_name }}</td>
|
|
<td class="tone-{{ (u.cpu_grade or status_class(u.cpu_share, 'cpu')) if u.cpus and u.cores is not none else 'dim' }}"
|
|
{% if u.cpus and u.cores is not none %}title="{{ '%.1f'|format(u.cpu_share) }}% of {{ r.host_name }}'s {{ u.cpus }} cores{% if u.cpu_usual %}; {{ u.cpu_usual }}{% endif %}"{% endif %}>
|
|
{{- bars(r.cpu_bars, (cores(r.cpu_bars.peak) ~ " cores") if r.cpu_bars.peak is not none else "", cores(r.cpu_bars.roof) ~ " cores") -}}
|
|
<span class="w-fig">{% if u.cores is none %}—{% else %}{{ cores(u.cores) }} cores{% endif %}</span>
|
|
</td>
|
|
<td class="tone-{{ (u.mem_grade or status_class(u.mem_pct, 'mem')) if u.mem_of and u.mem_bytes is not none else 'dim' }}"
|
|
{% if u.mem_pct is not none and u.mem_of and u.mem_bytes is not none %}title="{{ '%.1f'|format(u.mem_pct) }}% of {{ u.mem_of }}{% if u.mem_usual %}, {{ u.mem_usual }}{% endif %}"{% endif %}>
|
|
{{- bars(r.mem_bars, r.mem_bars.peak|bytes if r.mem_bars.peak is not none else "", r.mem_bars.roof|bytes) -}}
|
|
<span class="w-fig">{% if u.mem_bytes is none %}—{% else %}{{ u.mem_bytes|bytes }}{% endif %}</span>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
<div class="w-note w-wide-only">Bars: {{ (hours * 60 // 24) }}-minute slices, oldest left; each row up to its own peak, at least {{ cores(cpu_floor) }} core{{ 's' if cpu_floor != 1 }} and {{ mem_floor|bytes }}.</div>
|
|
{% endif %}
|