- Go 76.7%
- Shell 9.2%
- CSS 4.9%
- JavaScript 4.9%
- TeX 4.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| content | ||
| deploy | ||
| internal | ||
| scripts | ||
| static | ||
| .gitignore | ||
| go.mod | ||
| LICENSE | ||
| main.go | ||
| README.md | ||
| resume-proposed.pdf | ||
| resume-proposed.tex | ||
mdthomasel.dev
A personal site that is also its own proof of work. The server instruments itself and exposes Prometheus-format telemetry, and the homepage renders that telemetry live.
It has no third-party dependencies. go list -m all returns one module. No
web framework, no Prometheus client, no markdown parser, no charting library.
The exposition format, the histogram quantiles, the markdown renderer, and the
chart are all implemented here, because depending on them would contradict the
claim the page is making.
Why
Most portfolios assert competence. This one shows it. The homepage publishes a latency budget, and the histogram next to it either meets that budget or does not — there is no screenshot, and no number on the page that is not scraped from the running process.
The claim, measured
The site declares a p99 latency budget of 20ms and publishes its own histogram. Measured on a 2-vCPU Hetzner CX23:
| Load | p50 | p99 | max |
|---|---|---|---|
| 5,000 requests, 32 concurrent | 0.10ms | 0.50ms | — |
| 30,000 requests, 64 concurrent | 1.77ms | 8.42ms | 15.5ms |
The bucket boundary at exactly le="0.020" is deliberate: it makes the budget
directly readable from the exposed counts.
Endpoints
| Path | Purpose |
|---|---|
/ |
Landing page with the live panel |
/work/{slug} |
Project write-ups, rendered from markdown on disk |
/metrics |
Prometheus text exposition format |
/api/stats |
JSON for the homepage panel |
/api/decide |
Idempotent decision endpoint (POST) |
/healthz |
Liveness probe |
The exposition output is validated against Prometheus's own reference parser
(prometheus_client.parser), not only against the tests in this repo.
Architecture
main.go entrypoint, graceful shutdown, request deadlines
internal/telemetry/ hand-written counters, gauges, histograms, exposition
internal/content/ markdown loader and escaping-first renderer
internal/server/ routing, instrumentation middleware, idempotency store
content/*.md page sources
static/ CSS and the hand-rolled SVG chart
Notes on the interesting parts
Metric labels are bounded by route, not by URL. Every request to
/work/anything is recorded as path="/work/{slug}". Labelling by
r.URL.Path is the classic unbounded-cardinality bug, and it is what turns a
bot's random probing into unbounded memory growth. There is a test that hammers
50 distinct URLs and asserts the series count does not move.
Content is escaped before markup is generated. The renderer emits no markup
around unescaped text, and link URLs pass a scheme allowlist rather than merely
being HTML-escaped — html.EscapeString leaves javascript:alert(1)
completely intact, so escaping alone would not be a defence.
The histogram quantile accumulates buckets. A histogram stores per-bucket
counts; the quantile walks them cumulatively. Getting that wrong yields a
correct-looking /metrics output alongside a wrong p99, which is the worst
possible failure mode for a page whose entire claim is latency.
The idempotency store is bounded. An unbounded map keyed by client input is a memory-exhaustion vector, so the oldest entry is evicted at capacity. It is in-memory, which does not survive a restart or span instances; that limitation is deliberate and documented rather than hidden.
Development
go run . # serve on :8080
go test ./... # 56 tests
go test -race ./... # concurrency
go vet ./... && gofmt -l .
./scripts/build.sh # static binary, version-stamped
Deployment
One script, on the server, from a checkout:
./scripts/deploy.sh
It is idempotent. On a clean Debian or Ubuntu host it installs everything and
deploys; on a configured host it skips the host steps and goes straight to
building. It needs root, because it writes to /etc.
./scripts/deploy.sh --skip-host # skip package installation and host setup
./scripts/deploy.sh --help
The phases run in a deliberate order, and it matters:
- Host prerequisites — Go, Caddy, the
portfoliouser, directories, firewall, swap. Idempotent and cheap, so they run on every deploy. - Build —
go vet,gofmt, the full test suite, then a static-trimpathbinary. Tests run before anything is installed. - Configuration — the systemd unit and Caddyfile are installed from the repository, before the binary.
- Cross-check — every flag in the unit's
ExecStartis checked against the binary's own usage output. - Smoke test — the new binary runs on a spare port and must answer its health check, expose a valid histogram, and serve a decision, before it is allowed to replace the live one.
- Install and restart.
- Verify each layer separately — app, Caddy, the résumé, and the public TLS endpoint.
Steps 3 and 4 exist because the previous design split provisioning from deploying. One script installed the unit, the other replaced the binary, and nothing checked that they agreed — so a flag removed from the code stayed in the unit until it produced a restart loop. Installing configuration from the repository in a fixed order, then verifying it against the binary, removes that failure mode.
deploy/portfolio.service runs the binary as a non-root user with
ProtectSystem=strict, a PrivateTmp, a syscall filter, and a 256MB memory
cap. deploy/Caddyfile terminates TLS and proxies to loopback, so the Go
server's port is never exposed, and serves the résumé directly from disk so it
survives an app restart.
Licence
MIT. See LICENSE.