carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1{{define "app_logs"}}{{template "top" .}}
2{{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}}
3{{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}}
4{{template "appnav" .}}
5{{template "lognav" .}}
6
7<h2 class="visually-hidden">Logs</h2>
8{{if .LogsErr}}<p class="banner error">{{.LogsErr}}</p>{{end}}
9<form method="get" class="small" id="logs-filter" data-url="{{.Base}}/apps/{{.AppName}}/logs/data" data-cursor="{{.LogCursor}}" data-follow-error="{{.LogCopy.FollowError}}" data-refreshed="{{.LogCopy.Refreshed}}">
10 <label>{{.LogCopy.Instance}}
11 <select name="host">
12 <option value="">{{.LogCopy.AllInstances}}</option>
13 {{range .LogHosts}}<option value="{{.}}"{{if eq . $.LogHost}} selected{{end}}>{{.}}</option>{{end}}
14 </select>
15 </label>
16 <label>Show
17 <select name="stream">
18 <option value=""{{if eq .LogStream ""}} selected{{end}}>everything</option>
19 <option value="app"{{if eq .LogStream "app"}} selected{{end}}>app output</option>
20 <option value="platform"{{if eq .LogStream "platform"}} selected{{end}}>platform events</option>
21 </select>
22 </label>
23 <label>from the last
24 <select name="since">
25 {{if and (ne .LogSince "15m") (ne .LogSince "1h") (ne .LogSince "6h") (ne .LogSince "24h") (ne .LogSince "168h")}}<option value="{{.LogSince}}" selected>{{.LogSince}}</option>{{end}}
26 <option value="15m"{{if eq .LogSince "15m"}} selected{{end}}>15 minutes</option>
27 <option value="1h"{{if eq .LogSince "1h"}} selected{{end}}>hour</option>
28 <option value="6h"{{if eq .LogSince "6h"}} selected{{end}}>6 hours</option>
29 <option value="24h"{{if eq .LogSince "24h"}} selected{{end}}>day</option>
30 <option value="168h"{{if eq .LogSince "168h"}} selected{{end}}>week</option>
31 </select>
32 </label>
33 <label>{{.LogCopy.AllLevels}}
34 <select name="level">
35 <option value=""{{if eq .LogLevel ""}} selected{{end}}>{{.LogCopy.AllLevels}}</option>
36 <option value="debug"{{if eq .LogLevel "debug"}} selected{{end}}>debug</option>
37 <option value="info"{{if eq .LogLevel "info"}} selected{{end}}>info</option>
38 <option value="warn"{{if eq .LogLevel "warn"}} selected{{end}}>warn</option>
39 <option value="error"{{if eq .LogLevel "error"}} selected{{end}}>error</option>
40 </select>
41 </label>
42 <label>{{.LogCopy.Search}}
43 <input type="search" name="text" value="{{.LogGrep}}">
44 </label>
45 {{if .LogLegacyGrep}}<input type="hidden" name="grep" value="{{.LogLegacyGrep}}">{{end}}
46 <button type="submit">Update</button>
47 <button type="button" id="log-follow">{{.LogCopy.Follow}}</button>
48 <button type="button" id="log-pause" hidden>{{.LogCopy.Pause}}</button>
49</form>
50
51{{if .LogTruncated}}<p class="small">Showing the newest {{len .LogRows}} lines of this window — narrow the window, or use <code>carlos logs</code> for more.</p>{{end}}
52<div class="term" id="logterm" role="log" aria-label="Log output" tabindex="0" aria-live="polite">
53{{- range logLines .LogRows -}}
54<div class="tl{{if .Tone}} tl-{{.Tone}}{{end}}"><span class="tl-t">{{.Time}}</span> <span class="tl-s">{{.Stream}}</span>{{if .Host}} <span class="tl-h">{{.Host}}</span>{{end}} <span class="tl-m">{{.Msg}}</span>{{if .Fields}}<span class="tl-f">{{.Fields}}</span>{{end}}</div>
55{{- end -}}
56{{if and (not .LogRows) (not .LogsErr)}}<div id="log-empty">No log lines in this window. <a href="{{.LogWidenURL}}">{{.LogCopy.Widen}}</a></div>{{end}}
57</div>
58<p class="small" id="log-status" aria-live="polite"></p>
59{{if .LogRows}}
60<p class="small">Times are UTC. Follow live with <code>carlos logs --app {{.AppName}} -f</code>.</p>
61{{else if not .LogsErr}}
62<p class="small">An app's output appears here shortly after it runs; platform events
63(wakes, restarts, failures) appear even when the app itself printed nothing. A
64site served from files alone has no app output.</p>
65{{end}}
66<script>
67(function () {
68 var form = document.getElementById("logs-filter");
69 var term = document.getElementById("logterm");
70 var follow = document.getElementById("log-follow");
71 var pause = document.getElementById("log-pause");
72 var status = document.getElementById("log-status");
73 if (!form || !term || !follow || !pause || !status) return;
74 var cursor = form.dataset.cursor || "";
75 var timer = 0;
76 var request = null;
77 var fetchTimeout = 0;
78 var timedOut = false;
79 var following = false;
80 var generation = 0;
81 var dirty = false;
82 var activeQuery = new URLSearchParams(new FormData(form));
83
84 function setFollowing(on) {
85 var moveFocus = document.activeElement === (on ? follow : pause);
86 following = on;
87 follow.hidden = on;
88 pause.hidden = !on;
89 var q = new URLSearchParams(activeQuery);
90 if (on) q.set("follow", "1"); else q.delete("follow");
91 history.replaceState(null, "", location.pathname + "?" + q.toString());
92 if (moveFocus) (on ? pause : follow).focus();
93 }
94 function addSpan(row, cls, value, prefix) {
95 if (!value) return;
96 if (prefix) row.appendChild(document.createTextNode(prefix));
97 var span = document.createElement("span");
98 span.className = cls;
99 span.textContent = value;
100 row.appendChild(span);
101 }
102 function append(line) {
103 var empty = document.getElementById("log-empty");
104 if (empty) empty.remove();
105 var row = document.createElement("div");
106 row.className = "tl" + (line.tone ? " tl-" + line.tone : "");
107 addSpan(row, "tl-t", line.time, "");
108 addSpan(row, "tl-s", line.stream, " ");
109 addSpan(row, "tl-h", line.host, " ");
110 addSpan(row, "tl-m", line.msg, " ");
111 addSpan(row, "tl-f", line.fields, "");
112 term.appendChild(row);
113 while (term.querySelectorAll(".tl").length > 200) term.querySelector(".tl").remove();
114 term.scrollTop = term.scrollHeight;
115 }
116 function clearLines() {
117 term.querySelectorAll(".tl").forEach(function (row) { row.remove(); });
118 }
119 function stop() {
120 generation++;
121 following = false;
122 if (timer) window.clearTimeout(timer);
123 timer = 0;
124 if (request) request.abort();
125 request = null;
126 if (fetchTimeout) window.clearTimeout(fetchTimeout);
127 fetchTimeout = 0;
128 timedOut = false;
129 term.setAttribute("aria-busy", "false");
130 setFollowing(false);
131 }
132 function poll() {
133 if (!following) return;
134 var thisGeneration = generation;
135 var q = new URLSearchParams(activeQuery);
136 var hadCursor = Boolean(cursor);
137 q.set("limit", "200");
138 if (cursor) {
139 q.set("cursor", cursor);
140 q.delete("since");
141 q.delete("until");
142 }
143 request = new AbortController();
144 timedOut = false;
145 var thisRequest = request;
146 fetchTimeout = window.setTimeout(function () {
147 if (following && thisGeneration === generation && request === thisRequest) {
148 timedOut = true;
149 thisRequest.abort();
150 }
151 }, 20000);
152 term.setAttribute("aria-busy", "true");
153 fetch(form.dataset.url + "?" + q.toString(), {signal: request.signal, headers: {"Accept": "application/json"}})
154 .then(function (r) { if (!r.ok) throw new Error(String(r.status)); return r.json(); })
155 .then(function (data) {
156 if (!following || thisGeneration !== generation) return;
157 window.clearTimeout(fetchTimeout);
158 fetchTimeout = 0;
159 if (data.gap) {
160 cursor = "";
161 stop();
162 status.textContent = form.dataset.followError;
163 return;
164 }
165 if (!hadCursor) clearLines();
166 data.lines.forEach(append);
167 cursor = data.cursor || cursor;
168 status.textContent = form.dataset.refreshed + " " + new Date().toLocaleTimeString();
169 term.setAttribute("aria-busy", "false");
170 timer = window.setTimeout(poll, 2000);
171 })
172 .catch(function (err) {
173 window.clearTimeout(fetchTimeout);
174 fetchTimeout = 0;
175 if (err.name === "AbortError" && !timedOut) return;
176 if (!following || thisGeneration !== generation) return;
177 stop();
178 status.textContent = form.dataset.followError;
179 });
180 }
181 form.addEventListener("input", function () { dirty = true; if (following) stop(); });
182 form.addEventListener("change", function () { dirty = true; if (following) stop(); });
183 follow.addEventListener("click", function () {
184 if (dirty) {
185 var q = new URLSearchParams(new FormData(form));
186 q.set("follow", "1");
187 location.assign(location.pathname + "?" + q.toString());
188 return;
189 }
190 setFollowing(true);
191 poll();
192 });
193 pause.addEventListener("click", stop);
194 term.scrollTop = term.scrollHeight;
195 {{if .LogFollow}}setFollowing(true); poll();{{end}}
196})();
197</script>
198{{template "bottom" .}}{{end}}
199