Two writes that failed at the same time put their reports back in the wrong order when the older one failed first. A mutex is now held across each write, from taking the waiting reports out of the buffer to putting them back if the write fails, so the next write takes them in order. The write Append starts at the 10 MiB threshold waits for one under way, and Append starts no other while it waits. Files written since the start are now deleted to make room in the order they were written. A test that needed two writes at once is removed; new tests cover the one waiting write and the order files are deleted in. Model: opus-5-5
33 KiB
NetWatch is an MIT-licensed JavaScript single-page application by @sneak that provides real-time network latency monitoring to common internet hosts, displayed with color-coded figures and sparkline graphs, served from a static bucket or from its Docker image, where a small Go backend stores the measurements the page reports.
Getting Started
# Install the dependencies and the git pre-commit hook
make setup
# Run the page on the Vite dev server
make dev
# Build netwatch-server and run it on port 8080
make run
# Run the tests, both linters and the format check
make check
# Build the page into dist/ and netwatch-server into the repo root
make build
# Build the image and run it
make docker
docker run -p 8080:8080 netwatch
make check and make docker need Docker. make dev passes /api to
http://127.0.0.1:8080, where make run starts netwatch-server with its
defaults, so the reports the page posts are stored in data/reports.
Entrypoints
This repository adheres to the
Scripts to Rule Them All
standard: normalized scripts in script/ are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call them.
The scripts cover both the frontend and the Go backend, so make check fails if
either one is broken. We provide:
script/bootstrap— install all dependencies (the pinned node via nvm unless one new enough for the frontend's dependencies is installed, yarn via corepack,yarn install --frozen-lockfile, GoGO_VERSIONunless thegoonPATHis exactly that version, and the Go modules), linking what it installs itself into~/.local/bin, which has to be onPATH. It installs no Go linter and not Docker:make testandmake lintrun in Dockerscript/setup— make a fresh clone ready for development: bootstrap plus the git pre-commit hookscript/dev— run the Vite dev server, which proxies/apito a locally runningnetwatch-serverscript/build— build the frontend for production intodist/, then the staticnetwatch-serverbinary into the repo root with its version stamped in:VERSIONfrom the environment, or when that is unset or empty,git describeinside a git checkout, thendevscript/run— build withscript/build, then runnetwatch-serverwith its defaults (see Settings)script/clean— remove whatscript/buildwritesscript/projectname— print the project name (used for the Docker image tag)script/test— build thetestphase ofDockerfile, writing no image: the frontend's unit tests in itsfrontend-teststage, and the backend's Go tests with the race detector and coverage. The frontend's production build is left to the image build, in thefrontend-buildstagescript/lint— build thelintphase ofDockerfile, writing no image: eslint in itsfrontend-lintstage, and golangci-lint over the Go packagesscript/fmt— format all files (writes): prettier over the JavaScript, CSS, HTML and Markdown, then gofmt overcmd/andinternal/. It runs on the host, asscript/fmt-checkdoes, with~/.local/binput onPATHscript/fmt-check— check formatting (read-only): prettier, then gofmtscript/check— run test, lint, and fmt-checkscript/add-dependency— add a frontend package, or move one to another version:make add-dependency PACKAGE=<name>@<version>runsyarn add --dev, which changespackage.jsonandyarn.locktogether, thenyarn install --frozen-lockfilescript/tidy— rungo mod tidy: to add a Go module, import it and runmake tidy; to move one to another version, edit itsrequireline ingo.mod, then runmake tidyscript/frontend-test— run the unit tests intest/unit/on the host with Node's built-in test runner, through thetestscript inpackage.json, and if any fails, run them again listing every test, and fail; then the production build. Each test run has a 90-second timeout.make testruns the same tests in Docker, without the production buildscript/frontend-fmt— format the JavaScript, CSS, HTML and Markdown with prettier (writes)script/frontend-fmt-check— check prettier formatting (read-only)script/frontend-viewport-test— responsive-layout verification of the built frontend in a containerised headless Chrome (see test/viewport/README.md). Before the browser starts, it checks that the page, and a path that falls back to it, are served withCache-Control: no-cacheandContent-Security-Policy. Not part ofscript/check: it takes minutes.script/docker— build the image fromDockerfile, taggednetwatchviascript/projectnamescript/cibuild— CI entrypoint: runsscript/bootstrapandscript/check, then builds the image asscript/dockerdoesscript/precommit— run by the git pre-commit hook; runsscript/checkscript/install-precommit— install the git pre-commit hook
script/test, script/lint, script/docker and script/cibuild each pass a
new CACHEBUST value, so every step in Dockerfile that lints, tests or
compiles runs again even when nothing has changed. Docker keeps the system
packages and the Go modules from earlier builds until the base image, the step
that installs them, or go.mod and go.sum change. The frontend-build stage
is the exception: it installs git again on every image build, because it starts
from the frontend-test stage, whose steps run again on every build. Go's build
cache and golangci-lint's are kept as well, and each reuses a result only when
everything it was made from is unchanged. Both go test runs carry -count=1,
so no test result is reused. The yarn packages are installed again on every
build.
Responsive layout
The narrow-viewport layout lives in the max-width: 768px media block in
web/src/styles.css. It is verified automatically by
make frontend-viewport-test, which drives a digest-pinned headless Chrome
against the built dist/ and asserts on computed layout at widths derived from
that CSS — on every breakpoint it declares and one pixel either side of it, plus
a 320px floor, a desktop baseline and two landscape sizes. See
test/viewport/README.md for what it covers and what
it genuinely cannot.
Rationale
When debugging network issues, it's useful to have a persistent at-a-glance view of latency and reachability to multiple well-known internet endpoints. NetWatch provides this as a single page that does all its measuring in the browser, so it can be served from anywhere static files are served. The backend in its Docker image only stores the measurements the page reports; without it, the page works the same and nothing is stored.
Design
The page's source is in web/, the Go backend's in cmd/ and internal/, and
nginx's configuration in configs/.
The page is built with Vite and Tailwind CSS v4. Its code is all in
web/src/main.js, with a class-based architecture:
CONFIG: Configuration object (update interval, timeouts, axis ticks, etc.). The interval menu setsupdateInterval, the one value the page writes intoCONFIG; the timeouts, the time the history spans and the x-axis ticks are computed from itHostState: Per-host state management — history buffer, latency tracking, status transitionsAppState: Top-level state container — WAN hosts, local hosts, pause state, aggregate statsSparklineRenderer: Canvas 2D sparkline drawing with fixed axes, color-coded line segments, error regions, and DPR-aware scaling- UI functions:
buildUI()constructs the DOM,updateHostRow()/updateSummary()/updateHealthBox()handle incremental updates tick(): Main loop — measures all hosts in parallel, pushing each host's sample and redrawing its row as soon as its check ends, then redraws every row, the summary and the health box once the last check ends. The first round, after loading or an interval change, is discarded. The rows are sorted when the last check ends in round 2, the first one kept, and in rounds 11, 21, 31 and so on. When paused, pushes blank markers (no probes, no false outage)Reporter: Posts collected samples to the backend
Reporting
Every reportInterval (default 60s) the page POSTs a JSON report to the
same-origin path /api/v1/reports: a random per-browser clientId kept in
localStorage, geo sent as null, and each host's unreported, non-paused
samples (timestamp, latency, error). A per-host high-water mark makes every
report a delta, so only new samples are sent; the mark advances only on a
delivered report, and while paused nothing is sent. Changing the interval clears
each host's history on the page; the samples in it not yet reported still go in
the next report. Delivery failure is quiet — one debug-log line per outage,
never blocking probing. The report-building step is a pure function of host
state.
A sample in the history that no report has delivered is sent only as long as the history holds it, so after a refused or failed report the next report carries the samples the history still has. Samples an interval change cleared from the history are kept apart, the last 100 per host, until they are delivered. The history keeps each host's last 100 rounds: 100 seconds at the 1-second interval, 200 seconds at 2 seconds, 5 minutes at the default of 3 seconds and 100 minutes at 60 seconds. At 1 second, one refused or failed report loses the oldest 20 seconds of each host's samples, as the next delivered report comes 2 minutes after the last. At 2 seconds and longer one loses nothing; samples are lost from the third refused report in a row at 2 seconds, from the fifth at 3 seconds, and later still at the longer intervals.
No report is sent while the page is paused, but each paused round still adds an entry to the history, so each report due during a pause counts as a refused one. A pause longer than the history spans loses the samples in the history recorded between the last delivered report and the pause: up to a minute's worth for each host if the report before the pause was delivered.
Backend
netwatch-server is a small Go HTTP server that stores the reports the page
posts. It keeps them in memory and writes them to DATA_DIR as zstd-compressed
files of JSON lines: every minute, whenever 10 MiB are waiting, and when it
stops. Its routes:
POST /api/v1/reports— takes a report, without credentials; each client address may send a limited number a minute, and the report files are capped in size, the oldest deleted firstGET /.well-known/healthcheck— answers 200 with"status":"ok", the server's version and its uptimeGET /metrics— Prometheus metrics behind basic auth, only whenMETRICS_USERNAMEandMETRICS_PASSWORDare set; each client address may make a limited number of requests to it a minute
It is an fx-wired Go application: cmd/netwatch-server/ starts it, and its
packages are in internal/:
config: Loads configuration from environment variables and config files via Viper.handlers: HTTP request handlers for the API (health check, report ingestion).reportbuf: The in-memory buffer of reports and the files it writes them to.server: Chi-based HTTP server with middleware wiring and route registration.healthcheck,middleware,logger,globals: Supporting infrastructure.
In the image, the test phase of Dockerfile tests it, the builder stage
builds it, and bin/entrypoint.sh runs it as user netwatch on
127.0.0.1:8081, behind nginx (see Deployment). Outside the
image, make run builds it and runs it on port 8080.
Settings
| Variable | Default | Description |
|---|---|---|
BIND_ADDRESS |
empty | IP address to listen on; empty listens on every interface |
PORT |
8080 |
HTTP listen port |
DATA_DIR |
./data/reports |
Directory for compressed reports |
DATA_DIR_MAX_BYTES |
1073741824 (1 GiB) |
Most bytes of report files kept in DATA_DIR, oldest deleted first; see Report limits |
DEBUG |
false |
Enable debug logging |
TRUSTED_PROXIES |
RFC 1918 ranges | Comma-separated CIDRs whose X-Forwarded-For / X-Real-IP headers are trusted for client IP resolution |
REPORTS_PER_MINUTE |
60 |
Reports each client address may send a minute; see Report limits |
CORS_ALLOWED_ORIGINS |
empty | Comma-separated origins whose pages may call the API; see CORS |
METRICS_USERNAME |
empty | Basic auth user name for /metrics; see Metrics |
METRICS_PASSWORD |
empty | Basic auth password for /metrics; see Metrics |
SENTRY_DSN |
empty | DSN of the Sentry project to send errors to; see Sentry |
TRUSTED_PROXIES defaults to 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16. A list
replaces the default; set and empty, the server trusts no proxy. Loopback is not
in the default, so a reverse proxy on the same host has to be named, as in
TRUSTED_PROXIES=127.0.0.1/32. A request whose direct peer is outside this set
has its forwarded headers ignored, and the direct peer is logged and
rate-limited instead. From a direct peer inside the set, the client address is
the right-most X-Forwarded-For entry that is not in the set: each proxy
appends the address it took the request from, so the entries further left were
written by the client and are not used. An entry written as an address with a
port, such as 198.51.100.7:5000 or [2001:db8::1]:443, is read as that
address. If the search from the right finds only addresses in the set, or meets
an entry that is not an address at all, such as unknown, the direct peer is
used. X-Real-IP is read only when X-Forwarded-For is missing or empty. The
container image does not use this default; see Deployment.
A variable set to a value the server cannot use, such as PORT=abc,
DEBUG=maybe or a BIND_ADDRESS that is not an IP address, stops it from
starting, with an error naming the variable. An empty variable counts as unset,
except TRUSTED_PROXIES.
Report storage
Reports are written as reports-<timestamp>-<number>.jsonl.zst files in
DATA_DIR. The timestamp is in UTC to the millisecond, so the names sort by
time. The number starts at 1 when the server starts and goes up by one for each
file the server starts to write, so two files written in the same millisecond
still get different names. A failed write uses up its number and leaves a gap in
the numbers: its file, if it was created, is removed. The file stays, counted
toward DATA_DIR_MAX_BYTES from the next start, only if removing it fails too.
Files are created with O_EXCL to prevent overwrites.
The reports of a failed write stay in memory, and the next flush, within a
minute, tries again to write them. The server writes one file at a time, so a
file holds its reports in the order they arrived, those of a failed write
included. From the failed write until a write succeeds, POST /api/v1/reports
refuses every report with 500 and keeps nothing of it. The page sends the
refused samples again in its next report, as long as it still holds them: at the
1-second interval one refused report already loses some of those in the history
(see Reporting). Reports already answered 200 are lost only if the
server stops before a write succeeds; the final flush at stop tries them too,
and if it fails, the process exits non-zero.
Report limits
POST /api/v1/reports takes reports from anyone who can reach it, without
credentials, so it is bounded instead. Each refusal below answers with the same
{"status":"error"} body as any other error, except nginx's refusal of a body
over the limit in the image.
- JSON only. A report must be sent with
Content-Type: application/json, parameters such ascharsetallowed, as the page sends it; any other is refused with 415, and nothing of it is stored. A page on another origin can send atext/plainPOST, or a form, without a CORS preflight; refusing them keeps any site from making its visitors' browsers store reports (see CORS). - Body limit. A report body may be at most 1 MiB, the backend's limit for
any request. Run on its own, the backend answers 413 before the route to a
request that declares a longer body. Without a declared length, the route
answers 413 only when the report it reads runs past 1 MiB; nothing after the
end of the report is read, so a longer body whose report ends sooner is
stored, and one that stops being valid JSON sooner is answered 400. In the
image, nginx refuses a body over 1 MiB first, with or without a declared
length, as its
client_max_body_size, whichconfigs/nginx.confdoes not set, is 1 MiB by default. nginx answers 413 with its own HTML error page, and the backend never sees the request. - Rate limit. Each client address, resolved through
TRUSTED_PROXIES, may sendREPORTS_PER_MINUTEreports a minute; past that it gets 429 withRetry-After: 60. The minute slides: reports from the minute before still count, fading out over the current one, so an address is sure never to be refused only while it sends at most half ofREPORTS_PER_MINUTEin any 60 seconds. The page sends one report a minute from each open tab, so the default of 60 refuses nothing from up to 30 tabs behind one address, such as a household or an office sharing it, however their reports bunch up. Report responses also carryX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Resetheaders. - Size cap. The report files in
DATA_DIRmay total at mostDATA_DIR_MAX_BYTES, counting the files already there at start. Reports waiting in memory count at their uncompressed size until they are written, those of a failed write included; the part of its file written is removed. When a report would take the total past the cap, the oldest report files are deleted to make room, and each deletion is logged with the file's name and size; a file still being written is never deleted. A report is refused with 507, and nothing of it is stored, only when the reports waiting to be written fill the cap on their own, and then no file is deleted. At start, report files past the cap, as after lowering it, are deleted the same way. So the cap is how much of the newest reports is kept: the default of 1 GiB is small enough for any host; set it to the space you can giveDATA_DIR.
At the defaults, one address may send 60 reports of up to 1 MiB a minute, and a
report made up to compress badly takes about as much room in the report files as
its request body, so in about 17 minutes one address can fill the 1 GiB cap with
its own reports, every report stored before them deleted to make room. Lowering
REPORTS_PER_MINUTE or raising DATA_DIR_MAX_BYTES makes that take longer.
CORS
The page calls the API from the origin it is served from, so by default the
server sends no CORS headers, and browsers let no other origin's pages call it.
To serve the page from elsewhere, list that origin in CORS_ALLOWED_ORIGINS
(for example https://netwatch.example.com); pages from a listed origin may
GET and POST with a Content-Type header. Each entry must be a plain
origin, scheme://host with an optional :port, as browsers send it: no path,
not even a trailing /, and no *. Any other entry stops the server from
starting, with an error naming CORS_ALLOWED_ORIGINS.
Metrics
With both METRICS_USERNAME and METRICS_PASSWORD set, the server serves
Prometheus metrics at GET /metrics to requests with those as their basic auth
credentials, and answers any other with 401. For each request that reaches the
health check or POST /api/v1/reports, those the rate limit refuses included,
the metrics record its duration and response size, labelled with its path,
method and status; they also count those requests in progress, and include Go's
runtime and process metrics. No other request is recorded: not those to
/metrics itself, and not those answered before they reach either route, such
as a CORS preflight, or a request refused with 404 for a path no route has or
405 for a method its route does not take. Run on its own, the backend also
answers 413 before the route to a request declaring a body length over the 1 MiB
limit, which is not recorded. A body over the limit sent without a declared
length reaches the route and is recorded with the status the route answers,
which is 413 only when the report it reads runs past the limit: nothing after
the end of the report is read. In the image neither reaches the backend: nginx
answers both with 413 itself (see Report limits), and nothing
of them is recorded. Clients can make up any number of paths and methods, and
each would add labels to the metrics for as long as the server runs.
/metrics is rate limited, so that its password cannot be guessed quickly: each
client address, resolved through TRUSTED_PROXIES, may make 60 requests to it a
minute, whatever their credentials. Past that it gets 429 with
Retry-After: 60, and its credentials are not checked. The minute slides as it
does for reports (see Report limits), so a scraper polling
every 2 seconds or less often is never refused. This allowance is apart from the
one for reports.
Sentry
With SENTRY_DSN set, each panic in a handler is reported to that Sentry
project under the release netwatch-server- followed by the server's version.
On shutdown the server waits up to 2 seconds for Sentry to finish sending.
Monitoring targets
- 26 WAN hosts: datavi.be (pinned at start), Anthropic API, OpenAI API, AWS Console, Google Cloud Console, Microsoft Azure, Cloudflare, Fastly CDN, Akamai, Google, GitHub, B2, 8 S3 regional endpoints (Cape Town, London, Bahrain, Tokyo, Singapore, Sydney, Oregon, São Paulo) and 6 Hetzner speed test servers (Nuremberg, Falkenstein, Helsinki, Ashburn, Hillsboro, Singapore)
- Local CPE: Cable modem at 192.168.100.1 (always monitored)
- Local Gateway: Auto-detected on startup by probing common default gateway addresses (192.168.1.1, 192.168.0.1, 192.168.8.1, 10.0.0.1); first responder wins. Note: modern browsers enforce Private Network Access restrictions that block public-origin pages from reaching RFC1918 addresses, so local targets only work when NetWatch is served from localhost or a private address.
Local hosts are tracked separately from WAN stats.
Latency measurement
GET requests to each target's URL as written, with mode: 'no-cors' and
cache: 'no-store', timed with performance.now(). No query string is added:
the Hetzner speed-test servers close the connection without an answer when the
URL has one, and no-store keeps the browser's cache out of the measurement.
Each check times out after 80% of the refresh interval (24 seconds at 30
seconds) and is then recorded as a timeout, so a round's checks have all
finished before the next round is due. When no WAN host answers, a recovery
probe checks 4 WAN hosts, picked at random when it starts, every half second,
giving up the checks it started half a second before. As soon as one answers, a
new round starts at once, as it does after an interval change. A round started
early gives up the last round's checks if they are still waiting, and that round
records nothing more, so rounds never overlap. The browser chooses between IPv4
and IPv6 for each target, as for any request; the local targets are IPv4
addresses.
Each recorded check that fails writes one line to the browser console with
console.error, and the same line to the debug log: the target's name and URL,
the time, whether it timed out, answered over the time limit or hit a network
error (with the error the browser gives the page, such as
TypeError: Failed to fetch, which does not say why), and how long the request
took. A target that answers after failed checks writes one console.info line
with its latency and how many checks in a row had failed.
Color coding
| Latency | Color |
|---|---|
| < 50ms | Green |
| < 100ms | Lime |
| < 200ms | Yellow |
| < 500ms | Orange |
| >= 500ms | Red |
| Unreachable | Gray |
Output structure
dist/
├── index.html
└── assets/
├── index-*.css
└── index-*.js
Features
- A round of checks every 3 seconds by default; the interval menu sets 1, 2, 3, 5, 10, 15, 30 or 60 seconds and clears the history
- Sparklines of each target's last 100 rounds: 300 seconds at 3 seconds
- The first round after loading or an interval change is discarded, as DNS and TLS setup inflate its latencies
- Health indicator from the WAN hosts' latest results: OFFLINE (red) when more than 10 fail and at most 4 answer, otherwise DEGRADED (orange) when more than 4 fail, otherwise SLOW (yellow) when more than 3 take over 1000ms, otherwise HEALTHY (green)
- Summary stats across WAN hosts only: how many answered, the min, median,
average and max of their latest latencies, the min and max over the whole
history, and the number of rounds run (
Checks) - Fixed chart axes: Y-axis 0–1000ms, higher latencies drawn at the top; X-axis the time the history spans
- Color-coded latency figures and sparkline line segments
- WAN host rows sorted by latest latency, unreachable last; pinned rows stay on top, in name order
- Play/pause: pause stops probes but history keeps scrolling (blank gaps, no false outage)
- Debug log panel, behind a checkbox in the footer, with five levels (error, warning, notice, info, debug) and the last 1000 lines
- Local and UTC clocks
- Clickable service URLs
- A footer link to the commit the page was built from
- Canvas-based sparkline rendering with devicePixelRatio scaling
- Zero runtime dependencies: all resources bundled into build artifacts
Deployment
make build writes the page to dist/, which any static file host (S3, GCS,
Cloudflare Pages, Vercel, Netlify, GitHub Pages) can serve; with no backend
there, its reports fail quietly and nothing is stored. Or run the Docker image
behind a reverse proxy.
The Docker image, built from Dockerfile by make docker, is the whole service
in one container: nginx serves the built frontend and passes /api/,
/.well-known/healthcheck and /metrics to the Go backend, netwatch-server.
bin/entrypoint.sh starts it listening only inside the container, on
127.0.0.1:8081, with TRUSTED_PROXIES=127.0.0.1/32, so it takes the client
address nginx passes on and no other. The image:
- Listens on port 8080 by default (override with
PORTenv var) - Takes the client address from
X-Forwarded-Foronly on requests from the reverse proxies named inTRUSTED_PROXIES, by default from any address in10.0.0.0/8,172.16.0.0/12or192.168.0.0/16.bin/entrypoint.shchecks each entry withnetwatch-server check-cidr, which parses it as the backend parses its ownTRUSTED_PROXIES - Sends access logs to stdout
- Sends the page with
Cache-Control: no-cache, so a browser that keeps a copy checks with the server that it is current before using it again; only the files under/assets/, whose names change whenever their contents do, are cached as immutable - Sends the security headers
REPO_POLICIES.mdrequires on every response, asconfigs/security-headers.confsets them, in place of the backend's own - Stores reports in
DATA_DIR,/data/reportsby default, on the/datavolume. Before the backend starts, the image createsDATA_DIRand gives it and/datato usernetwatch(uid 1000), which the backend runs as, withnetwatch-server prepare-data-dir, which acts on nothing outside/data, so a host directory bind-mounted at/dataends up owned by uid 1000 - Writes buffered reports to disk on
docker stop, and exits non-zero if nginx or the backend exits on its own, so the platform restarts it
Running under upaas
What the upaas app for netwatch needs:
- Port: container port
8080. - Volume: container path
/data; the reports are kept in/data/reports. - Environment variables: none is required. An empty one counts as unset,
except
TRUSTED_PROXIES, and one set to a value netwatch cannot use stops the container at start, with the reason in its log.PORT, default8080: the container port, from 1 to 65535.8081cannot be used: the backend listens on it inside the containerREPORTS_PER_MINUTE, default60: reports each client address may send a minuteDATA_DIR_MAX_BYTES, default1073741824(1 GiB): the most room the report files may take; the oldest are deleted to stay under itCORS_ALLOWED_ORIGINS, default empty: other origins whose pages may call the APIDEBUG, defaultfalse: debug loggingDATA_DIR, default/data/reports: the directory the reports are kept in:/dataor a path below it, with no.or..part and no extra/. The container also stops if the path goes through a symbolic link that leads out of/dataor is written as a full pathTRUSTED_PROXIES, default10.0.0.0/8,172.16.0.0/12,192.168.0.0/16: the addresses the reverse proxy in front of the container connects from, as IP addresses or CIDRs separated by commas. nginx takes the client address fromX-Forwarded-Foronly on a request from one of them, and the rate limit counts that address. A list replaces the default. Set and empty,X-Forwarded-Foris ignored and every client behind the proxy shares the proxy's one allowance ofREPORTS_PER_MINUTE. Any client that connects from one of these addresses can write its ownX-Forwarded-For, and through a port Docker publishes, every client may connect from the Docker network's gateway, such as172.17.0.1, which the default includes; where that matters, name only the addresses nothing but the proxy connects from.METRICS_USERNAMEandMETRICS_PASSWORD, default empty: with both set, the backend records Prometheus metrics of its requests and serves them at/metricson the container port, to requests with this user name and password as their basic auth credentials. With neither set, there are no metrics and/metricsis not found. One set without the other, or a user name containing:, stops the containerSENTRY_DSN, default empty: set to a Sentry project's DSN, the backend sends its errors to that Sentry project: each request whose handling crashes, which still gets a 500 response. A value Sentry does not accept stops the container. Empty, the backend sends nothing to Sentry
- Health check: the image's
HEALTHCHECKrequests/.well-known/healthcheckthrough nginx every 30 seconds, so it fails unless both nginx and the backend answer. upaas reads the container's health 60 seconds after a deploy and fails the deploy unless it ishealthy. The container also stops when either process exits.
Browser Compatibility
Requires a modern browser with ES modules, Fetch API, Canvas API, and CSS custom properties.
Limitations
- CORS: The checks are cross-origin requests in
no-corsmode, so the page cannot read the answer, only time it: any answer counts as reachable, an error page included. - Local targets: The cable modem at 192.168.100.1 and the detected gateway answer only on a network that has them, and only when NetWatch is served from localhost or a private address (see Monitoring targets).
- Network conditions: Measurements reflect browser-to-endpoint latency, which includes your local network, ISP, and internet routing.
TODO
The to-do list is TODO.md: where the work stands, the next step, the open work, and what has been done.
License
MIT. See LICENSE.