Prometheus gains a "Widget templates" tab beside Targets. It lists the built-in templates and an admin's own, any that no longer pass the checker first. A template opens as its YAML beside a live preview drawn over a chosen target exactly as the dashboard widget draws it (routes.draw, now shared by both). An admin writes or pastes a new one, reads a .yaml upload into the editor to look over, duplicates any template under a free name, edits and deletes their own; anyone can export the YAML. Built-in templates are read-only. Custom templates are rows in prometheus_widget_templates (migration prometheus_002_templates), kept in backups, keyed "custom:<id>" so a rename keeps its widgets. Saving needs a template that passes the checker and a name no other template has. Create, update and delete are audited and link to the template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
4.6 KiB
HTML
75 lines
4.6 KiB
HTML
{# plugins/prometheus/templates/prometheus/target.html — one scrape target
|
|
(milestone 506): what its newest scrape found, then every series it
|
|
stored, grouped by metric, each with its labels and newest value and a
|
|
link to its chart. A state set reads as one row per thing it describes,
|
|
naming the current state (#5689). #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs, page_header, status_badge, stat_tile %}
|
|
{% block title %}Prometheus — {{ target.name }} — Steward{% endblock %}
|
|
{% from "prometheus/_tabs.html" import prometheus_tabs %}
|
|
{% block section_tabs %}{{ prometheus_tabs("targets") }}{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Prometheus", "/plugins/prometheus/"), (target.name, "")]) }}{% endblock %}
|
|
{% block content %}
|
|
{% set admin = session.user_role == 'admin' %}
|
|
{% call page_header(target.name, target.url) %}
|
|
{% if admin %}<a href="/plugins/prometheus/targets/{{ target.id }}/edit" class="btn btn-sm btn-ghost">Edit</a>{% endif %}
|
|
{% endcall %}
|
|
|
|
<p class="page-verdict">
|
|
{% if target.reach == 'up' %}{{ status_badge('up', 'Answering') }} scraped {{ target.scraped_at|ago }}, every {{ target.interval|duration }}
|
|
{% elif target.reach == 'down' %}{{ status_badge('down', 'Not answering') }} <span class="tone-crit">{{ target.says }}</span>
|
|
{% if target.ok_at %}Last answered {{ target.ok_at|ago }}; the values below are from then.{% else %}It has never answered.{% endif %}
|
|
{% elif target.reach == 'stale' %}{{ status_badge('stale', 'Not scraped') }} {{ target.says }}
|
|
{% elif target.reach == 'off' %}{{ status_badge('stale', 'Turned off') }} {{ target.says }}
|
|
{% else %}{{ status_badge('pending', 'Not scraped yet') }} The first scrape runs within seconds of saving.{% endif %}
|
|
</p>
|
|
{% if target.capped %}
|
|
<p class="tone-warn">{{ target.capped }} series weren't stored on the last scrape: past the cap of {{ target.cap }}, or a name too long to store.
|
|
{% if admin %}<a href="/plugins/prometheus/targets/{{ target.id }}/edit">Narrow what's kept or raise the cap.</a>{% endif %}</p>
|
|
{% endif %}
|
|
|
|
{% if target.reach != 'pending' %}
|
|
<div class="stat-strip">
|
|
{{ stat_tile("Series stored", target.kept) }}
|
|
{{ stat_tile("Over the cap", target.capped, state="degraded" if target.capped else none) }}
|
|
{{ stat_tile("Left out by filters", target.dropped) }}
|
|
{{ stat_tile("Scrape time", (target.scrape_ms|round|int ~ " ms") if target.scrape_ms is not none else "—") }}
|
|
</div>
|
|
{% endif %}
|
|
<p class="field-hint">Keeping {{ target.keep|join(", ") if target.keep else "everything" }}{% if target.drop %}, leaving out {{ target.drop|join(", ") }}{% endif %}.
|
|
Alert rules can watch any of these under <strong>Prometheus targets</strong>.</p>
|
|
|
|
<section class="card" aria-labelledby="prom-series">
|
|
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.5rem;">
|
|
<h2 id="prom-series" class="section-title" style="margin:0;">Series <span class="tone-muted">{{ total }}</span></h2>
|
|
<form method="get" style="margin-left:auto;display:flex;gap:0.5rem;">
|
|
<label class="sr-only" for="prom-q">Find a metric</label>
|
|
<input class="input" id="prom-q" name="q" value="{{ q }}" placeholder="Find a metric" style="max-width:16rem;">
|
|
{% if q %}<a class="btn btn-sm btn-ghost" href="/plugins/prometheus/targets/{{ target.id }}">Clear</a>{% endif %}
|
|
</form>
|
|
</div>
|
|
{% if not groups %}
|
|
<p class="field-hint" style="margin:0;">{% if q %}No stored metric matches “{{ q }}”.{% elif target.reach == 'pending' %}Nothing stored yet.{% else %}Nothing stored: the filters leave every series out, or the target hasn't answered.{% endif %}</p>
|
|
{% else %}
|
|
<table class="table">
|
|
<thead><tr><th scope="col">Metric</th><th scope="col">Labels</th><th scope="col" style="text-align:right;">Now</th></tr></thead>
|
|
<tbody>
|
|
{% for g in groups %}
|
|
{% for r in g.rows %}
|
|
<tr>
|
|
{% if loop.first %}<td class="mono" rowspan="{{ g.rows|length }}" style="vertical-align:top;overflow-wrap:anywhere;">{{ g.metric }}
|
|
{%- if g.states %}<div class="cell-sub">a state set: the current state is shown</div>{% endif %}</td>{% endif %}
|
|
<td class="mono" style="overflow-wrap:anywhere;font-size:0.82rem;">{{ r.labels or "—" }}</td>
|
|
<td style="text-align:right;white-space:nowrap;">
|
|
<a href="/plugins/prometheus/targets/{{ target.id }}/series?resource={{ r.resource|urlencode }}&metric={{ g.metric|urlencode }}"
|
|
{%- if r.states %} title="One of: {{ r.states|join(', ') }}"{% endif %}>{% if r.moment %}{{ r.moment|ago }}{% else %}{{ r.text }}{% endif %}</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% endif %}
|
|
</section>
|
|
{% endblock %}
|