@cryptotaxi247 / infra / commits / 58c7a00d

status.nixos.org: add channel status quick-view

Graham Christensen committed Dec 30, 2019 at 20:50 UTC 58c7a00d1ec6f3b12e6ff585e480f2e7df439e59
4 files changed +301 -6
delft/eris.nix
+1 -6
@@ -43,12 +43,7 @@ in {
43 virtualHosts."status.nixos.org" = {
44 enableACME = true;
45 forceSSL = true;
46 - root = pkgs.writeTextDir "index.html" ''
47 - <ul>
48 - <li><a href="/grafana">Grafana</a></li>
49 - <li><a href="/prometheus">Prometheus</a></li>
50 - </ul>
51 - '';
46 + root = ./eris/status-page;
47 locations."/grafana/".proxyPass = "http://${config.services.grafana.addr}:${toString config.services.grafana.port}/";
48 locations."/prometheus".proxyPass = "http://${config.services.prometheus.listenAddress}";
49 };
delft/eris/status-page/index.html new
+84
@@ -0,0 +1,84 @@
1 +<!DOCTYPE html>
2 +
3 +<html lang="en">
4 +
5 + <head>
6 + <title>NixOS Infra Status</title>
7 +
8 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
9 + <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
10 +
11 + <script type="text/javascript" src="https://nixos.org/js/jquery.min.js"></script>
12 + <script type="text/javascript" src="https://nixos.org/js/jquery-ui.min.js"></script>
13 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
14 + <script type="text/javascript" src="https://nixos.org/bootstrap/js/bootstrap.min.js"></script>
15 + <link rel="stylesheet" href="https://nixos.org/bootstrap/css/bootstrap.min.css" />
16 + <link rel="stylesheet" href="https://nixos.org/bootstrap/css/bootstrap-responsive.min.css" />
17 + <link rel="stylesheet" href="https://nixos.org/css/nixos-site.css" type="text/css" />
18 + <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" />
19 + <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment-with-locales.min.js" integrity="sha256-AdQN98MVZs44Eq2yTwtoKufhnU+uZ7v2kXnD5vqzZVo=" crossorigin="anonymous"></script>
20 + <link rel="shortcut icon" type="image/png" href="https://nixos.org/favicon.png" />
21 + <script type="text/javascript" src="/status.js"></script>
22 + <link rel="stylesheet" href="/status.css" />
23 +
24 + </head>
25 +
26 + <body>
27 +
28 + <div class="navbar navbar-fixed-top">
29 + <div class="navbar-inner">
30 + <div class="container">
31 + <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
32 + <span class="icon-bar"></span>
33 + <span class="icon-bar"></span>
34 + <span class="icon-bar"></span>
35 + </button>
36 + <li class="dropdown brand">
37 + <a class="dropdown-toggle" href="#" data-toggle="dropdown">
38 + <img src="https://nixos.org/logo/nix-wiki.png" alt="NixOS" class="logo" /> NixOS Status <b class="caret"></b>
39 + <ul class="dropdown-menu">
40 + <li><a href="https://nixos.org">NixOS.org</a></li>
41 + </ul>
42 + </a>
43 + </li>
44 +
45 + <div class="nav-collapse collapse">
46 + <ul class="nav pull-left">
47 + <li><a href="/grafana">Grafana</a></li>
48 + <li><a href="/prometheus">Prometheus</a></li>
49 + </ul>
50 + </div>
51 + </div>
52 + </div>
53 + </div>
54 +
55 + <div class="container main">
56 + <div class="nixos-org-index">
57 + <h1>NixOS</h1>
58 + </div>
59 +
60 + <div class="hero12">
61 + <h2>Nix Channel Status</h2>
62 + <p>See <a href="http://howoldis.herokuapp.com/">HowOldIs</a> and the <a href="https://nixos.wiki/wiki/Nix_channels">NixOS Wiki</a> for information on how channel updates progress.</p>
63 + </p>
64 + </p>
65 + </div>
66 +
67 + <div class="hero12">
68 + <table class="table table-striped">
69 + <thead>
70 + <tr>
71 + <th>Channel</th>
72 + <th>Last updated</th>
73 + <th>Commit</th>
74 + <th>Hydra job for tests</th>
75 + <th>Channel Status</th>
76 + </tr>
77 + </thead>
78 + <tbody id="channel-status"></tbody>
79 + </table>
80 + </div>
81 + </div>
82 + </body>
83 +
84 +</html>
delft/eris/status-page/status.css new
+29
@@ -0,0 +1,29 @@
1 +
2 +.stale {
3 + opacity: 60%;
4 +}
5 +
6 +.channel {
7 + font-family: monospace;
8 + max-width: 20em;
9 +}
10 +
11 +.agetd {
12 + max-width: 15em;
13 +}
14 +
15 +.github {
16 + max-width: 10em;
17 + overflow: hidden;
18 + text-overflow: ellipsis;
19 + font-family: monospace;
20 +}
21 +
22 +.hydra {
23 + max-width: 20em;
24 +}
25 +
26 +#channel-status {
27 + max-width: 100%;
28 +}
29 +
delft/eris/status-page/status.js new
+187
@@ -0,0 +1,187 @@
1 +function aggregateByChannel(result) {
2 + return result.reduce((acc, {
3 + channel,
4 + value
5 + }) => ({
6 + ...acc,
7 + [channel]: value
8 + }), {});
9 +}
10 +
11 +async function fetchData(queryType, queryArgs = {}) {
12 + const params = new URLSearchParams();
13 + for (const [key, value] of Object.entries(queryArgs)) {
14 + params.set(key, String(value));
15 + }
16 +
17 + const response = await fetch(`https://status.nixos.org/prometheus/api/v1/${queryType}?${params}`);
18 + const {
19 + data
20 + } = await response.json();
21 +
22 + return data.result;
23 +}
24 +
25 +const revisionData = fetchData('query', {
26 + query: 'channel_revision'
27 + })
28 + .then(records => (
29 + records.map(({
30 + metric
31 + }) => ({
32 + channel: metric.channel,
33 + value: {
34 + revision: metric.revision,
35 + github_url: `https://github.com/NixOS/nixpkgs/commit/${metric.revision}`,
36 + },
37 + }))
38 + ))
39 + .then(aggregateByChannel);
40 +
41 +
42 +const updateTimeData = fetchData('query', {
43 + query: 'channel_update_time'
44 + })
45 + .then(records => (
46 + records.map(({
47 + metric,
48 + value
49 + }) => ({
50 + channel: metric.channel,
51 + value: {
52 + update_time: value[1],
53 + },
54 + }))
55 + ))
56 + .then(aggregateByChannel);
57 +
58 +var earliestStart = moment.utc("2019-12-30T01:00:00Z");
59 +var idealStart = moment.utc().subtract(30, "days");
60 +var start;
61 +if (idealStart > earliestStart) {
62 + start = idealStart;
63 +} else {
64 + start = earliestStart;
65 +}
66 +var end = moment.utc().format();
67 +const jobsetData = fetchData('query_range', {
68 + query: 'hydra_job_failed',
69 + start: start.format(),
70 + end,
71 + step: '1h'
72 + })
73 + .then(records => (
74 + records.map(({
75 + metric,
76 + values
77 + }) => {
78 + const project = metric.project;
79 + const jobset = metric.jobset;
80 + const job = metric.exported_job;
81 +
82 + return {
83 + channel: metric.channel,
84 + value: {
85 + current: metric.current == 1,
86 + project,
87 + jobset,
88 + job,
89 + job_history: values.map(state => state[1] == 0),
90 + hydra_url: `https://hydra.nixos.org/job/${project}/${jobset}/${job}`,
91 + },
92 + };
93 + })
94 + ))
95 + .then(aggregateByChannel);
96 +
97 +function split_channel(channel) {
98 + var parts = channel.split("-");
99 + return {
100 + "time": parts[1],
101 + "collection": parts[0],
102 + "qualifier": parts[2] || "",
103 + }
104 +}
105 +
106 +function normalize_channel(channel) {
107 + parts = split_channel(channel);
108 + return [parts['time'], parts['collection'], parts['qualifier']].join("-");
109 +}
110 +
111 +function cmp_channels(left, right) {
112 + return normalize_channel(left) < normalize_channel(right)
113 +}
114 +
115 +Promise.all([revisionData, updateTimeData, jobsetData])
116 + .then(([revisions, update_times, jobsets]) => {
117 + var combined = [];
118 +
119 + for (let [channel, jobset] of Object.entries(jobsets)) {
120 + // Ensure each jobset here is in each other dataset, guaranteeing we have
121 + // complete data.
122 + jobset['channel'] = channel;
123 + if (revisions[channel] != undefined) {
124 + jobset['revision'] = revisions[channel]['revision'];
125 + jobset['github_url'] = revisions[channel]['github_url'];
126 + } else {
127 + continue
128 + }
129 + if (update_times[channel] != undefined) {
130 + var m = moment.unix(update_times[channel]['update_time']);
131 + jobset['update_time_relative'] = m.fromNow()
132 + jobset['update_time_local'] = m.format()
133 + if (m > moment().subtract(3, 'days')) {
134 + jobset['update_age'] = "success";
135 + } else if (m > moment().subtract(10, 'days')) {
136 + jobset['update_age'] = "warning";
137 + } else {
138 + jobset['update_age'] = "important";
139 + }
140 + } else {
141 + continue
142 + }
143 +
144 + combined.push(jobset);
145 + }
146 +
147 + combined.sort((left, right) => cmp_channels(left['channel'], right['channel']));
148 +
149 + return combined;
150 + })
151 + .then(data => {
152 + return data.map(record => {
153 + var row = document.createElement('tr');
154 + row.innerHTML = '<td class="channel" /><td><span class="age label"></span></td><td class="github"><a class="revision" /></td><td class="hydra" title="The state of the job over time"><a class="hydra-link" /></td><td class="status"></td>';
155 + if (record['current']) {
156 + row.classList.add("current")
157 + } else {
158 + row.classList.add("stale")
159 + row.getElementsByClassName("status")[0].innerHTML += '<span class="label label-important">End of life</span>';
160 + }
161 + row.getElementsByClassName("channel")[0].innerText = record['channel'];
162 +
163 + row.getElementsByClassName("age")[0].innerText = record['update_time_relative'];
164 + row.getElementsByClassName("age")[0].title = record['update_time_local'];
165 + row.getElementsByClassName("age")[0].classList.add("label-" + record['update_age']);
166 + row.getElementsByClassName("revision")[0].innerText = record['revision'];
167 + row.getElementsByClassName("revision")[0].href = record['github_url'];
168 +
169 + row.getElementsByClassName("hydra-link")[0].href = record['hydra_url'];
170 + row.getElementsByClassName("hydra-link")[0].innerText = [record['project'], record['jobset'], record['job']].join('/');
171 + row.getElementsByClassName("hydra")[0].style.backgroundImage = "linear-gradient(to right, " +
172 + (record['job_history'].map(val => val ? "#b5ffb5" : "#ff9e9e")).join(", ") +
173 + ")";
174 +
175 + if (record['job_history'][record['job_history'].length - 1] == 0) {
176 + row.getElementsByClassName("status")[0].innerHTML += '<span class="label label-important">Failing</span>';
177 +
178 + }
179 + return row;
180 + })
181 + })
182 + .then(rows => {
183 + var tbody = document.getElementById("channel-status");
184 + rows.forEach(row => {
185 + tbody.appendChild(row);
186 + });
187 + });