| 1 |
{% extends "base.html" %} |
| 2 |
{% import "repo_tabs.html" as m %} |
| 3 |
{% block title %}Insights · {{ scope }}{% endblock %} |
| 4 |
{% block bodyattrs %}{% if wall %} class="wall"{% endif %}{% endblock %} |
| 5 |
{% block tabs %}{% if !wall %}{% if let Some(r) = repo %}{% call m::tabs(r, "insights") %}{% endif %}{% endif %}{% endblock %} |
| 6 |
{% block content %} |
| 7 |
<div class="insights" data-refresh="30" data-fragment="insights"> |
| 8 |
<div class="head"><h1>Insights <small>{{ scope }} · OxygenFlow, the last {% if range_on("1h") %}hour{% else if range_on("7d") %}7 days{% else %}24 hours{% endif %}</small></h1></div> |
| 9 |
<div class="bar insights-bar"> |
| 10 |
<span class="modes"> |
| 11 |
<a href="{{ base }}?range=1h{{ keep }}"{% if range_on("1h") %} class="on" aria-current="page"{% endif %}>1 hour</a> |
| 12 |
<a href="{{ base }}?range=24h{{ keep }}"{% if range_on("24h") %} class="on" aria-current="page"{% endif %}>24 hours</a> |
| 13 |
<a href="{{ base }}?range=7d{{ keep }}"{% if range_on("7d") %} class="on" aria-current="page"{% endif %}>7 days</a> |
| 14 |
</span> |
| 15 |
{% if !repo_choices.is_empty() || !runner_choices.is_empty() %} |
| 16 |
<form class="filters" method="get" action="{{ base }}"> |
| 17 |
<input type="hidden" name="range" value="{{ i.range.key() }}">{% if wall %}<input type="hidden" name="wall" value="1">{% endif %} |
| 18 |
<select name="repo" aria-label="Repository or owner"><option value="">every repository</option>{% for r in repo_choices %}<option{% if *r == repo_filter %} selected{% endif %}>{{ r }}</option>{% endfor %}</select> |
| 19 |
<select name="runner" aria-label="Runner"><option value="">every runner</option>{% for r in runner_choices %}<option{% if *r == runner_filter %} selected{% endif %}>{{ r }}</option>{% endfor %}</select> |
| 20 |
<button>filter</button> |
| 21 |
</form> |
| 22 |
{% endif %} |
| 23 |
<span class="insights-links small"> |
| 24 |
{% if let Some((label, url)) = wider %}<a href="{{ url }}?range={{ i.range.key() }}">{{ label }}</a>{% endif %} |
| 25 |
{% if wall %}<a href="{{ base }}?range={{ i.range.key() }}{% if !repo_filter.is_empty() %}&repo={{ repo_filter|urlencode }}{% endif %}{% if !runner_filter.is_empty() %}&runner={{ runner_filter|urlencode }}{% endif %}">leave the wall view</a>{% else %}<a href="{{ base }}?range={{ i.range.key() }}{{ keep }}&wall=1" target="_blank" rel="noopener" title="For a second monitor: large, no menus, refreshing itself">wall view ↗</a>{% endif %} |
| 26 |
{% if !grafana.is_empty() %}<a href="{{ grafana }}" target="_blank" rel="noopener">open in Grafana ↗</a>{% endif %} |
| 27 |
</span> |
| 28 |
</div> |
| 29 |
|
| 30 |
<div class="kpis"> |
| 31 |
<div class="kpi"><span class="kpi-v">{{ secs(i.pickup_median) }}</span><span class="kpi-l">time to pickup, median</span><span class="kpi-s">95th percentile {{ secs(i.pickup_p95) }}</span></div> |
| 32 |
<div class="kpi"><span class="kpi-v">{{ i.ended }}</span><span class="kpi-l">jobs finished</span><span class="kpi-s">{{ pass_rate() }} passed of those that passed or failed</span></div> |
| 33 |
<div class="kpi"><span class="kpi-v">{{ secs(i.duration_median) }}</span><span class="kpi-l">job duration, median</span><span class="kpi-s">started to finished</span></div> |
| 34 |
<div class="kpi{% if i.queued_now > 0 && i.slots_total > 0 && i.slots_busy >= i.slots_total %} kpi-warn{% endif %}"><span class="kpi-v">{{ i.queued_now }}</span><span class="kpi-l">queued now</span><span class="kpi-s">{{ i.running_now }} running</span></div> |
| 35 |
<div class="kpi{% if i.runners_total > 0 && i.runners_online == 0 %} kpi-bad{% endif %}"><span class="kpi-v">{{ i.runners_online }}/{{ i.runners_total }}</span><span class="kpi-l">runners online</span><span class="kpi-s">{{ i.slots_busy }} of {{ i.slots_total }} slots busy</span></div> |
| 36 |
</div> |
| 37 |
|
| 38 |
<div class="chart-grid"> |
| 39 |
<figure class="chart-card"><figcaption>Time to pickup <span class="mute small">ready to started</span></figcaption>{{ charts.pickup|safe }}<div class="legend">{% for s in i.pickup %}<span class="key {{ s.class }}">{{ s.name }}</span>{% endfor %}</div></figure> |
| 40 |
<figure class="chart-card"><figcaption>Jobs finished <span class="mute small">per {% if range_on("1h") %}minute{% else if range_on("7d") %}3 hours{% else %}30 minutes{% endif %}</span></figcaption>{{ charts.results|safe }}<div class="legend">{% for s in i.results %}<span class="key {{ s.class }}">{{ s.name }}</span>{% endfor %}</div></figure> |
| 41 |
<figure class="chart-card"><figcaption>Queued and running</figcaption>{{ charts.load|safe }}<div class="legend">{% for s in i.load %}<span class="key {{ s.class }}">{{ s.name }}</span>{% endfor %}</div></figure> |
| 42 |
<figure class="chart-card"><figcaption>Job duration <span class="mute small">started to finished</span></figcaption>{{ charts.durations|safe }}<div class="legend">{% for s in i.durations %}<span class="key {{ s.class }}">{{ s.name }}</span>{% endfor %}</div></figure> |
| 43 |
<figure class="chart-card"><figcaption>Runner utilisation <span class="mute small">running jobs over the slots of the runners online now</span></figcaption>{{ charts.utilisation|safe }}<div class="legend">{% for s in i.utilisation %}<span class="key {{ s.class }}">{{ s.name }}</span>{% endfor %}</div></figure> |
| 44 |
</div> |
| 45 |
|
| 46 |
{% if !i.runners.is_empty() %} |
| 47 |
<section class="panel"><h2>Runners</h2> |
| 48 |
<div class="table"><table> |
| 49 |
<tr><th>Runner</th><th>Status</th><th>Busy</th><th>Jobs</th><th>Failed</th><th>Pickup, median</th><th>Duration, median</th><th>Last heard from</th></tr> |
| 50 |
{% for r in i.runners %}<tr><td><strong>{{ r.name }}</strong></td><td><span class="runner-status st-{{ r.status }}">{{ r.status }}</span></td><td>{{ r.running }}/{{ r.max_jobs }}</td><td>{{ r.jobs }}</td><td>{% if r.failed > 0 %}{{ r.failed }}{% else %}<span class="mute">0</span>{% endif %}</td><td>{{ secs(r.pickup_median) }}</td><td>{{ secs(r.duration_median) }}</td><td class="small">{% if let Some(t) = r.last_seen_ms %}<time title="{{ t|when_i }}">{{ t|ago_i }}</time>{% else %}<span class="mute">never</span>{% endif %}</td></tr>{% endfor %} |
| 51 |
</table></div> |
| 52 |
</section> |
| 53 |
{% endif %} |
| 54 |
|
| 55 |
{% if i.repos.len() > 1 %} |
| 56 |
<section class="panel"><h2>Repositories <small class="mute">busiest first</small></h2> |
| 57 |
<div class="table"><table> |
| 58 |
<tr><th>Repository</th><th>Jobs</th><th>Failed</th><th>Queued now</th><th>Pickup, median</th><th>Pickup, 95th percentile</th></tr> |
| 59 |
{% for r in i.repos %}<tr><td><a href="/{{ r.slug }}/insights?range={{ i.range.key() }}">{{ r.slug }}</a></td><td>{{ r.jobs }}</td><td>{% if r.failed > 0 %}{{ r.failed }}{% else %}<span class="mute">0</span>{% endif %}</td><td>{{ r.queued }}</td><td>{{ secs(r.pickup_median) }}</td><td>{{ secs(r.pickup_p95) }}</td></tr>{% endfor %} |
| 60 |
</table></div> |
| 61 |
</section> |
| 62 |
{% endif %} |
| 63 |
<p class="small mute">Updated <time title="{{ i.now_ms|when_i }}">{{ i.now_ms|ago_i }}</time>; this page refreshes itself every 30 seconds. Pickup is from when a job could run (queued, its dependencies passed, approved) to when a runner took it.</p> |
| 64 |
</div> |
| 65 |
{% endblock %} |