1 Commits
Author SHA1 Message Date
sneak 0809337c63 Log fx's startup steps at debug; a refused setting is one line (closes #209)
check / check (push) Canceled after 0s
fx's event lines, errors included, now log at debug level, so a normal
start shows none and DEBUG shows them. RunServer starts and stops the
fx app itself instead of calling Run: when the start fails, it logs the
error's root cause as one "cannot start" line, the backend's last, and
returns 1. Main returns the server's exit code.

Model: opus-5-5
2026-11-11 20:33:01 +00:00
5 changed files with 27 additions and 106 deletions

No files matched your search

+7 -7
View File
@@ -113,19 +113,19 @@ half second. As soon as one answers, a new round starts.
### Rows, colors and controls
A figure and its line in the graph take the color of the legend in the footer,
from green under 50 ms to red from 500 ms up; gray is no answer. The dot beside
the host's name takes the same color. The graph's axis ends at 1000 ms; a higher
latency is drawn at the top, and the figure still shows its real value.
from green under 50 ms to red from 500 ms up; gray is no answer. The graph's
axis ends at 1000 ms; a higher latency is drawn at the top, and the figure still
shows its real value.
From time to time, the page sorts the WAN rows by their latest latency: fastest
first, failed last. The pin button at the left of a row keeps the row at the
top; pinned rows are sorted by name. `datavi.be` is pinned at start.
`Pause` stops the checks: the graphs keep scrolling with a gap for each paused
round, and the figures and dots turn gray. The `Interval` menu sets the time
between rounds: 1, 2, 3, 5, 10, 15, 30 or 60 seconds. Changing it clears the
history and starts a new round at once. The `Debug log` checkbox in the footer
opens a panel with the page's last 1000 log lines.
round, and the figures turn gray. The `Interval` menu sets the time between
rounds: 1, 2, 3, 5, 10, 15, 30 or 60 seconds. Changing it clears the history and
starts a new round at once. The `Debug log` checkbox in the footer opens a panel
with the page's last 1000 log lines.
## Reporting
+5 -44
View File
@@ -56,7 +56,7 @@ func TestOutputIsJSONWithoutFxLines(t *testing.T) {
}
// TestDebugLogsFxLines: with DEBUG set, fx's lines are logged, as
// JSON, from the start to the signal that stops the server.
// JSON.
func TestDebugLogsFxLines(t *testing.T) {
t.Parallel()
@@ -64,17 +64,14 @@ func TestDebugLogsFxLines(t *testing.T) {
requireJSONLines(t, stdout, stderr)
for _, msg := range []string{"started", "received signal"} {
if !strings.Contains(stdout.String(), `"msg":"`+msg+`"`) {
t.Fatalf("no fx line %q in stdout with DEBUG set:\n%s",
msg, stdout)
}
if !strings.Contains(stdout.String(), `"msg":"started"`) {
t.Fatalf("no fx line in stdout with DEBUG set:\n%s", stdout)
}
}
// TestRefusedSettingIsLastLine: a setting the server refuses stops the
// start, and the one error line logged is the last, naming the setting
// and why, without fx's text around it.
// start, and the last line logged is one error naming the setting and
// why, without fx's text around it.
func TestRefusedSettingIsLastLine(t *testing.T) {
t.Parallel()
@@ -95,12 +92,6 @@ func TestRefusedSettingIsLastLine(t *testing.T) {
lines := strings.Split(strings.TrimSpace(stdout.String()), "\n")
for _, line := range lines[:len(lines)-1] {
if strings.Contains(line, `"level":"ERROR"`) {
t.Fatalf("an error line before the last line:\n%s", stdout)
}
}
var last struct {
Level string `json:"level"`
Error string `json:"error"`
@@ -220,36 +211,6 @@ func TestRefusedSentryDSNStopsTheStart(t *testing.T) {
}
}
// TestPortInUseExitsWithStatus1: a server whose port is already taken
// cannot listen, and stops with exit status 1.
func TestPortInUseExitsWithStatus1(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), childTimeout)
defer cancel()
var lc net.ListenConfig
taken, err := lc.Listen(ctx, "tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer func() { _ = taken.Close() }()
_, port, err := net.SplitHostPort(taken.Addr().String())
if err != nil {
t.Fatal(err)
}
child, stdout, _ := startServer(ctx, t, t.TempDir(), port)
err = child.Wait()
if child.ProcessState.ExitCode() != 1 {
t.Fatalf("server exit = %v, want exit status 1\n%s", err, stdout)
}
}
// startServer runs main() in a child process listening on
// 127.0.0.1:port, with home as its HOME and working directory and its
// data directory in home, so it touches nothing outside home. env is
+2 -7
View File
@@ -117,10 +117,7 @@ func RunServer(appname, version string) int {
globals.Appname = appname
globals.Version = version
var (
log *logger.Logger
fxLog *fxevent.SlogLogger
)
var log *logger.Logger
app := fx.New(
// fx logs each step of starting and stopping, errors included,
@@ -128,7 +125,7 @@ func RunServer(appname, version string) int {
// only with DEBUG set, and off a terminal they are JSON like
// every other line.
fx.WithLogger(func(log *logger.Logger) fxevent.Logger {
fxLog = &fxevent.SlogLogger{Logger: log.Get()}
fxLog := &fxevent.SlogLogger{Logger: log.Get()}
fxLog.UseLogLevel(slog.LevelDebug)
fxLog.UseErrorLevel(slog.LevelDebug)
@@ -175,9 +172,7 @@ func RunServer(appname, version string) int {
}
// A signal, or the server asking to stop after a listen error.
// fx's Run logs the signal here, so this does too.
shutdown := <-app.Wait()
fxLog.LogEvent(&fxevent.Stopping{Signal: shutdown.Signal})
stopCtx, cancelStop := context.WithTimeout(
context.Background(), app.StopTimeout())
+3 -40
View File
@@ -28,8 +28,8 @@ import {
// a host's row that tick or greyOutUI draws into is a plain object, made
// the first time a test looks it up and kept in elements under its
// selector until the next test starts. As on a page, writing its text
// replaces its markup; setting a style keeps it in the element's style
// object. Drawing a sparkline does nothing; it looks for the pixel ratio on
// replaces its markup; the status dot greyOutUI looks for in it is not
// there. Drawing a sparkline does nothing; it looks for the pixel ratio on
// window and finds none. The commit the page's footer names, which
// vite.config.js fills in when it builds the page, is blank. In each test,
// console.error and console.info print nothing and keep what they are
@@ -63,7 +63,7 @@ globalThis.document = {
querySelector: (selector) =>
(elements[selector] ??= {
getContext: () => canvasContext,
style: {},
querySelector: () => null,
set textContent(text) {
this.innerHTML = text;
},
@@ -83,12 +83,6 @@ function statusText(state, host) {
return elements[`.status-text[data-host="${index}"]`]?.innerHTML;
}
// The color last given to the dot beside host's name.
function statusDot(state, host) {
const index = state.allHosts.indexOf(host);
return elements[`.status-dot[data-host="${index}"]`]?.style.backgroundColor;
}
// What this test wrote to the browser console with console[method].
function consoleLines(method) {
return console[method].mock.calls.map((call) => call.arguments[0]);
@@ -689,37 +683,6 @@ test("a check that timed out or found its target unreachable has a grey figure a
assert.equal(latencyHex(null), "#6b7280");
});
test("a check that answers after 488ms turns the dot beside its target's name orange", async (t) => {
const { state, target } = stateWithOneTarget();
mockTargets(t, () => 488);
const round = tick(state, new AbortController().signal);
t.mock.timers.tick(488);
assert.notEqual(await settled(round), "still waiting");
assert.equal(statusDot(state, target), "#f97316");
});
test("a check that fails after one that answered turns the dot beside its target's name gray", async (t) => {
const { state, target } = stateWithOneTarget();
target.pushSample(Date.now(), { latency: 8, error: null });
mockTargets(t, () => 23, target.url);
const round = tick(state, new AbortController().signal);
t.mock.timers.tick(23);
assert.notEqual(await settled(round), "still waiting");
assert.equal(statusDot(state, target), "#6b7280");
});
test("pausing turns the dot beside a target's name gray", async (t) => {
const { state, target } = stateWithOneTarget();
mockTargets(t, () => 8);
const round = tick(state, new AbortController().signal);
t.mock.timers.tick(8);
assert.notEqual(await settled(round), "still waiting");
assert.equal(statusDot(state, target), "#22c55e");
state.paused = true;
greyOutUI(state);
assert.equal(statusDot(state, target), "#6b7280");
});
// buildUI draws the page into #app, here a plain object. Each entry in the
// footer's color legend is a colored dot and the latencies it stands for,
// with < and > written as &lt; and &gt;. A latency takes the color of the
+10 -8
View File
@@ -30,8 +30,8 @@ export const CONFIG = {
return [0, 1, 2, 3, 4, 5].map((i) => Math.round((d * i) / 5));
},
canvasHeight: 96,
// A latency figure, its row's dot and its sparkline take the color of
// the first entry whose limit, in ms, the latency is below.
// A latency figure and its sparkline take the color of the first entry
// whose limit, in ms, the latency is below.
latencyColors: [
{ below: 50, hex: "#22c55e", className: "text-green-500" },
{ below: 100, hex: "#84cc16", className: "text-lime-500" },
@@ -819,7 +819,7 @@ export function hostRowHTML(host, index, showPin = true) {
${pinBtn}
<div class="w-[420px] flex-shrink-0 grid grid-cols-[minmax(0,1fr)_auto] items-center">
<div class="flex items-center gap-2 min-w-[200px]">
<div class="status-dot w-3 h-3 rounded-full flex-shrink-0 bg-[#6b7280]" data-host="${index}"></div>
<div class="w-3 h-3 rounded-full flex-shrink-0 bg-[#6b7280]"></div>
<span class="font-medium text-white truncate">${escapeHTML(host.name)}</span>
</div>
<div class="latency-value text-4xl font-bold tabular-nums text-right mt-3" data-host="${index}">
@@ -982,10 +982,8 @@ function updateHostRow(host, index) {
const canvas = document.querySelector(
`.sparkline-canvas[data-host="${index}"]`,
);
const dot = document.querySelector(`.status-dot[data-host="${index}"]`);
if (!latencyEl || !statusEl || !canvas || !dot) return;
if (!latencyEl || !statusEl || !canvas) return;
dot.style.backgroundColor = latencyHex(host.lastLatency);
if (host.lastLatency !== null) {
const cls = latencyClass(host.lastLatency, host.status);
latencyEl.innerHTML = `<span class="${cls}">${host.lastLatency}<span class="text-lg">ms</span></span>`;
@@ -1422,8 +1420,12 @@ export function greyOutUI(state) {
statusEl.textContent = "paused";
statusEl.className = `${STATUS_TEXT_CLASS} text-gray-500`;
}
const dot = document.querySelector(`.status-dot[data-host="${i}"]`);
if (dot) dot.style.backgroundColor = latencyHex(null);
// Grey out the status dot
const row = document.querySelector(`.host-row[data-index="${i}"]`);
if (row) {
const dot = row.querySelector(".rounded-full");
if (dot) dot.style.backgroundColor = "#6b7280";
}
});
// Grey out summary current stats