@jimhoyd/urlcode 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +80 -0
- package/LICENSE +202 -0
- package/NOTICE +20 -0
- package/README.md +342 -0
- package/ROADMAP.md +248 -0
- package/SECURITY.md +40 -0
- package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
- package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
- package/data/agents/ai-crawlers.json +1240 -0
- package/data/agents/crawlers.json +10515 -0
- package/data/agents/index.d.ts +3 -0
- package/data/agents/index.js +2453 -0
- package/data/agents/monitoring.json +1520 -0
- package/data/agents/seo.json +3865 -0
- package/dist/BUILD-MANIFEST.json +60 -0
- package/dist/adapters.js +59 -0
- package/dist/agent-lists.js +59 -0
- package/dist/assets.js +129 -0
- package/dist/authoring.js +68 -0
- package/dist/aws.js +110 -0
- package/dist/build-cloudflare.js +164 -0
- package/dist/cli.js +185 -0
- package/dist/client-address.js +85 -0
- package/dist/cloudflare.js +165 -0
- package/dist/compliance-rules/baseline.js +98 -0
- package/dist/compliance-rules/privacy.js +38 -0
- package/dist/compliance-rules/shared.js +62 -0
- package/dist/compliance-rules/strict.js +61 -0
- package/dist/compliance.js +219 -0
- package/dist/config-worker.js +9 -0
- package/dist/config.js +172 -0
- package/dist/errors.js +8 -0
- package/dist/function-sources.js +54 -0
- package/dist/function-worker.js +105 -0
- package/dist/functions.js +174 -0
- package/dist/guest-api.js +114 -0
- package/dist/header-validation.js +18 -0
- package/dist/http-policy.js +74 -0
- package/dist/http-response.js +84 -0
- package/dist/index.js +11 -0
- package/dist/link-api.js +136 -0
- package/dist/link-cli.js +141 -0
- package/dist/link-events.js +76 -0
- package/dist/link-records.js +31 -0
- package/dist/link-store-worker.js +150 -0
- package/dist/link-store.js +250 -0
- package/dist/logging.js +22 -0
- package/dist/management-policy.js +41 -0
- package/dist/match.js +124 -0
- package/dist/observability.js +242 -0
- package/dist/plugins.js +74 -0
- package/dist/policies/agents.js +248 -0
- package/dist/policies/cache.js +297 -0
- package/dist/policies/compression.js +187 -0
- package/dist/policies/security.js +0 -0
- package/dist/policies/throttle.js +131 -0
- package/dist/policies.js +142 -0
- package/dist/policy.js +55 -0
- package/dist/prerender.js +165 -0
- package/dist/project-tests.js +23 -0
- package/dist/readiness.js +216 -0
- package/dist/router.js +166 -0
- package/dist/runtime.js +244 -0
- package/dist/scaffold.js +0 -0
- package/dist/scripts/operational-drills.js +75 -0
- package/dist/server.js +270 -0
- package/dist/site.js +0 -0
- package/dist/sqlite-version.js +6 -0
- package/dist/types/adapters.d.ts +12 -0
- package/dist/types/agent-lists.d.ts +5 -0
- package/dist/types/assets.d.ts +13 -0
- package/dist/types/authoring.d.ts +2 -0
- package/dist/types/aws.d.ts +35 -0
- package/dist/types/build-cloudflare.d.ts +14 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/client-address.d.ts +9 -0
- package/dist/types/cloudflare.d.ts +75 -0
- package/dist/types/compliance-rules/baseline.d.ts +12 -0
- package/dist/types/compliance-rules/privacy.d.ts +6 -0
- package/dist/types/compliance-rules/shared.d.ts +24 -0
- package/dist/types/compliance-rules/strict.d.ts +9 -0
- package/dist/types/compliance.d.ts +119 -0
- package/dist/types/config-worker.d.ts +1 -0
- package/dist/types/config.d.ts +21 -0
- package/dist/types/errors.d.ts +7 -0
- package/dist/types/function-sources.d.ts +21 -0
- package/dist/types/function-worker.d.ts +1 -0
- package/dist/types/functions.d.ts +92 -0
- package/dist/types/guest-api.d.ts +16 -0
- package/dist/types/header-validation.d.ts +2 -0
- package/dist/types/http-policy.d.ts +39 -0
- package/dist/types/http-response.d.ts +43 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/link-api.d.ts +30 -0
- package/dist/types/link-cli.d.ts +36 -0
- package/dist/types/link-events.d.ts +27 -0
- package/dist/types/link-records.d.ts +11 -0
- package/dist/types/link-store-worker.d.ts +1 -0
- package/dist/types/link-store.d.ts +130 -0
- package/dist/types/logging.d.ts +9 -0
- package/dist/types/management-policy.d.ts +9 -0
- package/dist/types/match.d.ts +81 -0
- package/dist/types/observability.d.ts +104 -0
- package/dist/types/plugins.d.ts +27 -0
- package/dist/types/policies/agents.d.ts +86 -0
- package/dist/types/policies/cache.d.ts +82 -0
- package/dist/types/policies/compression.d.ts +41 -0
- package/dist/types/policies/security.d.ts +37 -0
- package/dist/types/policies/throttle.d.ts +55 -0
- package/dist/types/policies.d.ts +36 -0
- package/dist/types/policy.d.ts +21 -0
- package/dist/types/prerender.d.ts +31 -0
- package/dist/types/project-tests.d.ts +13 -0
- package/dist/types/readiness.d.ts +121 -0
- package/dist/types/router.d.ts +11 -0
- package/dist/types/runtime.d.ts +91 -0
- package/dist/types/scaffold.d.ts +17 -0
- package/dist/types/server.d.ts +37 -0
- package/dist/types/site.d.ts +12 -0
- package/dist/types/sqlite-version.d.ts +1 -0
- package/dist/types/types.d.ts +332 -0
- package/dist/types/vercel.d.ts +12 -0
- package/dist/types.js +177 -0
- package/dist/vercel.js +65 -0
- package/docs/AI-AUTHORING.md +113 -0
- package/docs/ASSETS.md +106 -0
- package/docs/AWS.md +77 -0
- package/docs/BEST-PRACTICES.md +267 -0
- package/docs/CAPACITY.md +208 -0
- package/docs/CLOUDFLARE.md +110 -0
- package/docs/COMPLIANCE.md +242 -0
- package/docs/DYNAMIC-LINKS.md +561 -0
- package/docs/FUNCTION-SECURITY.md +113 -0
- package/docs/HTTP.md +129 -0
- package/docs/INSTALL.md +90 -0
- package/docs/LOAD-TESTING.md +91 -0
- package/docs/LOCAL-DEVELOPMENT.md +99 -0
- package/docs/MANAGEMENT-SECURITY.md +82 -0
- package/docs/MIDDLEWARE.md +83 -0
- package/docs/MONITORING.md +132 -0
- package/docs/OBSERVABILITY.md +229 -0
- package/docs/OPERATIONAL-PROOF.md +42 -0
- package/docs/OPERATIONS.md +207 -0
- package/docs/ORGANIZATION.md +135 -0
- package/docs/PERFORMANCE.md +72 -0
- package/docs/PLUGINS.md +234 -0
- package/docs/POLICIES.md +489 -0
- package/docs/PRERENDER.md +193 -0
- package/docs/PROJECT-DIRECTION.md +51 -0
- package/docs/READINESS.md +125 -0
- package/docs/README.md +58 -0
- package/docs/RELEASE-READINESS.md +95 -0
- package/docs/RELEASE-SECURITY.md +72 -0
- package/docs/RESILIENCE.md +160 -0
- package/docs/ROUTING.md +95 -0
- package/docs/SANDBOX-REVIEW.md +59 -0
- package/docs/SCAFFOLDING.md +72 -0
- package/docs/SECURITY-AUDIT.md +124 -0
- package/docs/SITE.md +150 -0
- package/docs/SPECIFICATION.md +232 -0
- package/docs/SPIKE-EXTENSIONS.md +475 -0
- package/docs/STANDARDS.md +303 -0
- package/docs/STARTERS.md +51 -0
- package/docs/TUNNELS.md +75 -0
- package/docs/TYPESCRIPT.md +102 -0
- package/docs/VERCEL.md +107 -0
- package/docs/YAML-GUIDE.md +521 -0
- package/docs/YAML-REFERENCE.md +328 -0
- package/docs/policies/agents.md +182 -0
- package/docs/policies/cache.md +139 -0
- package/docs/policies/compression.md +168 -0
- package/docs/policies/security.md +160 -0
- package/docs/policies/throttle.md +103 -0
- package/examples/assets/.env.example +1 -0
- package/examples/assets/.gitattributes +2 -0
- package/examples/assets/Makefile +30 -0
- package/examples/assets/README.md +18 -0
- package/examples/assets/functions/hello.mjs +3 -0
- package/examples/assets/gitignore.template +7 -0
- package/examples/assets/public/about.html +2 -0
- package/examples/assets/public/assets/example.txt +1 -0
- package/examples/assets/public/guide.txt +1 -0
- package/examples/assets/tests/requests.json +44 -0
- package/examples/assets/urlcode.yaml +27 -0
- package/examples/aws/README.md +13 -0
- package/examples/aws/handler.mjs +5 -0
- package/examples/aws/public/index.html +3 -0
- package/examples/aws/public/notes.txt +1 -0
- package/examples/aws/template.yaml +33 -0
- package/examples/aws/tests/requests.json +9 -0
- package/examples/aws/urlcode.yaml +19 -0
- package/examples/cloudflare/README.md +18 -0
- package/examples/cloudflare/package.json +14 -0
- package/examples/cloudflare/tests/requests.json +9 -0
- package/examples/cloudflare/urlcode.yaml +24 -0
- package/examples/cloudflare/wrangler.toml +5 -0
- package/examples/compliance/README.md +20 -0
- package/examples/compliance/rules.mjs +65 -0
- package/examples/cookbook/README.md +21 -0
- package/examples/cookbook/functions/choice.mjs +4 -0
- package/examples/cookbook/functions/echo.mjs +3 -0
- package/examples/cookbook/functions/hello.mjs +3 -0
- package/examples/cookbook/functions/text.mjs +3 -0
- package/examples/cookbook/middleware/headers.mjs +6 -0
- package/examples/cookbook/public/about.html +1 -0
- package/examples/cookbook/public/assets/index.html +1 -0
- package/examples/cookbook/public/assets/site.css +1 -0
- package/examples/cookbook/public/favicon.svg +1 -0
- package/examples/cookbook/public/guide.txt +1 -0
- package/examples/cookbook/public/llms.txt +6 -0
- package/examples/cookbook/routes/code.yaml +55 -0
- package/examples/cookbook/routes/files.yaml +17 -0
- package/examples/cookbook/routes/policies.yaml +28 -0
- package/examples/cookbook/routes/redirects.yaml +38 -0
- package/examples/cookbook/routes/responses.yaml +26 -0
- package/examples/cookbook/tests/requests.json +254 -0
- package/examples/cookbook/urlcode.yaml +22 -0
- package/examples/live-links/README.md +11 -0
- package/examples/live-links/tests/requests.json +6 -0
- package/examples/live-links/urlcode.yaml +16 -0
- package/examples/monitoring/blackbox-jobs.yaml +37 -0
- package/examples/monitoring/prometheus-rules.yaml +88 -0
- package/examples/monitoring/prometheus-scrape.yaml +20 -0
- package/examples/monitoring/vector.toml +65 -0
- package/examples/prerender/README.md +48 -0
- package/examples/prerender/functions/page.mjs +6 -0
- package/examples/prerender/middleware/template.mjs +29 -0
- package/examples/prerender/prerender.d.mts +4 -0
- package/examples/prerender/prerender.mjs +63 -0
- package/examples/prerender/tests/requests.json +39 -0
- package/examples/prerender/urlcode.yaml +50 -0
- package/examples/tunnel/dev-with-ngrok.sh +61 -0
- package/examples/vercel/README.md +13 -0
- package/examples/vercel/api/index.js +5 -0
- package/examples/vercel/package.json +8 -0
- package/examples/vercel/public/index.html +3 -0
- package/examples/vercel/public/notes.txt +1 -0
- package/examples/vercel/tests/requests.json +48 -0
- package/examples/vercel/urlcode.yaml +19 -0
- package/examples/vercel/vercel.json +10 -0
- package/llms.txt +46 -0
- package/package.json +123 -0
- package/schemas/urlcode.schema.json +1230 -0
- package/starters/default/.gitattributes +1 -0
- package/starters/default/Makefile +30 -0
- package/starters/default/README.md +34 -0
- package/starters/default/functions/hello.mjs +3 -0
- package/starters/default/gitignore.template +8 -0
- package/starters/default/middleware/headers.mjs +6 -0
- package/starters/default/routes/functions.yaml +20 -0
- package/starters/default/routes/marketing/links.yaml +7 -0
- package/starters/default/starter.json +5 -0
- package/starters/default/tests/requests.json +56 -0
- package/starters/default/urlcode.yaml +8 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Middleware
|
|
2
|
+
|
|
3
|
+
Middleware is reusable JavaScript around any route handler. It is optional and
|
|
4
|
+
route-local; plain redirects and assets retain their native fast path when no
|
|
5
|
+
middleware is attached. Adding middleware requires sandbox execution.
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
version: "1"
|
|
9
|
+
routes:
|
|
10
|
+
/go:
|
|
11
|
+
middleware:
|
|
12
|
+
- source: middleware/headers.mjs
|
|
13
|
+
export: default
|
|
14
|
+
redirect:
|
|
15
|
+
url: https://example.com
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default async function headers(request, context, next) {
|
|
20
|
+
context.state.startedAt = Date.now();
|
|
21
|
+
const response = await next();
|
|
22
|
+
response.headers.set('x-example-middleware', 'active');
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Paths resolve from the project root. Up to 16 entries are allowed; each accepts
|
|
28
|
+
`source` and optional `export` (default `default`). Reuse a module across routes.
|
|
29
|
+
There are no global middleware settings or middleware-specific argument fields.
|
|
30
|
+
This is URLCode's small portable API, not Express/Node middleware compatibility.
|
|
31
|
+
|
|
32
|
+
## Order and responses
|
|
33
|
+
|
|
34
|
+
For `[first, second]`, execution is first-before, second-before, handler,
|
|
35
|
+
second-after, first-after. Call `await next()` to obtain the downstream response.
|
|
36
|
+
Call it at most once, during the middleware invocation, with no arguments.
|
|
37
|
+
Always return a `Response`. Return early to skip downstream middleware and the
|
|
38
|
+
handler, for example `return new Response('Denied', {status: 403})`.
|
|
39
|
+
Middleware may catch downstream JavaScript exceptions and return a fallback.
|
|
40
|
+
|
|
41
|
+
Every middleware and the function share the same request and context. Validated
|
|
42
|
+
`inputs`, function `args`, and route-scoped `env`/`secrets` are available along
|
|
43
|
+
with a fresh `context.state` object for this chain. State never survives the
|
|
44
|
+
request. Header edits are visible downstream; editing the request does not
|
|
45
|
+
reroute it or change already validated inputs. Request bodies are single-use:
|
|
46
|
+
reading one consumes it for downstream code. There is no `clone()` or streaming
|
|
47
|
+
API; pass parsed data through `context.state` when needed.
|
|
48
|
+
|
|
49
|
+
Function responses support the existing text/JSON guest API. To transform their
|
|
50
|
+
body, read it and return a new `Response`. Native redirect/respond/link/page/static/
|
|
51
|
+
download bodies are opaque and cannot be read through `text()` or `json()`.
|
|
52
|
+
Returning the same native response preserves original bytes, including binary
|
|
53
|
+
files, ranges and HEAD lengths. You may add headers, but cannot change its
|
|
54
|
+
original status or existing native headers while preserving that body. To replace
|
|
55
|
+
status, destination or content, return a new `Response` instead. Replacement
|
|
56
|
+
responses follow the normal sandbox text/JSON and size limits. To wrap a shared
|
|
57
|
+
template around file content, render it through a function at build time and
|
|
58
|
+
publish the result: see [prerendering](PRERENDER.md).
|
|
59
|
+
|
|
60
|
+
Route selection, enabled/expiry checks, methods and input/body validation run
|
|
61
|
+
before middleware. Their errors do not pass through the chain. A missing file
|
|
62
|
+
inside a selected static mount is a downstream 404 response. Stored-link lookup
|
|
63
|
+
errors (missing, disabled, expired or unavailable store) precede middleware. YAML
|
|
64
|
+
`response.headers` apply last and override matching middleware headers. Runtime
|
|
65
|
+
framing and asset metadata protections still apply.
|
|
66
|
+
|
|
67
|
+
## Isolation and testing
|
|
68
|
+
|
|
69
|
+
The whole chain and handler run in one fresh QuickJS/WASM guest with one memory
|
|
70
|
+
budget and one deadline. No Node, filesystem, shell, fetch or ambient environment
|
|
71
|
+
is exposed. Modules can only access this route's declared dependency graphs.
|
|
72
|
+
All middleware receive that route's approved bindings, so review the whole chain;
|
|
73
|
+
source changes invalidate grants. See [security](FUNCTION-SECURITY.md).
|
|
74
|
+
|
|
75
|
+
Invalid responses and repeated `next()` calls fail with 502, exhausted capacity
|
|
76
|
+
returns 503, and the shared deadline returns 504. Middleware cannot extend the
|
|
77
|
+
deadline or catch the outer worker termination. Forgotten downstream work is
|
|
78
|
+
still drained within that deadline.
|
|
79
|
+
|
|
80
|
+
Include explicit request fixtures for middleware-wrapped routes: test success,
|
|
81
|
+
early responses, validation failures and every configured method. Audit will
|
|
82
|
+
report missing coverage instead of assuming native handler behavior. Benchmark
|
|
83
|
+
with middleware enabled to measure its actual sandbox overhead.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Monitoring a URLCode deployment
|
|
2
|
+
|
|
3
|
+
URLCode emits one JSON object per line on stdout, answers two unauthenticated
|
|
4
|
+
probes and, when an operator opts in, serves in-process counters in Prometheus
|
|
5
|
+
format. Everything below is built from those sources, and the example
|
|
6
|
+
configuration in [`examples/monitoring/`](../examples/monitoring/) is the
|
|
7
|
+
runnable form of it. The contract behind all of it (the event catalogue, the
|
|
8
|
+
observer interface for in-process sinks, the metrics snapshot and the
|
|
9
|
+
exposition endpoint) is in [observability](OBSERVABILITY.md).
|
|
10
|
+
|
|
11
|
+
What you can measure is shaped by a deliberate logging decision: records carry
|
|
12
|
+
**no URL, query string, header, body, binding or user exception text**. You can
|
|
13
|
+
alert on error rates and latency per configured route; you cannot get per-URL
|
|
14
|
+
analytics from these logs, and adding it would mean logging user data. See
|
|
15
|
+
[operations](OPERATIONS.md).
|
|
16
|
+
|
|
17
|
+
## Probes
|
|
18
|
+
|
|
19
|
+
| Endpoint | Meaning | Alert when |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `GET /_urlcode/health` | The process is alive and not shutting down. | It fails at all (`UrlcodeDown`). |
|
|
22
|
+
| `GET /_urlcode/ready` | The active snapshot, every function worker and every configured link-store reader are available. | It fails for longer than replacement takes (`UrlcodeNotReady`). |
|
|
23
|
+
|
|
24
|
+
Both return `{status, version, routes}`. A third endpoint,
|
|
25
|
+
`GET /_urlcode/metrics`, exists only with `startServer({ metrics: true })` and
|
|
26
|
+
answers 404 otherwise; see [metrics](#metrics) below. Keep both: alerting only on health hides
|
|
27
|
+
a process that is up and serving nothing, while alerting only on readiness pages
|
|
28
|
+
for a brief, self-healing worker replacement. Probes have their own bounded
|
|
29
|
+
admission budget (`--max-in-flight-health`), so they keep answering while the
|
|
30
|
+
application sheds load — but they are unauthenticated and disclose the
|
|
31
|
+
configuration digest and route count, so keep them internal.
|
|
32
|
+
|
|
33
|
+
## Log records
|
|
34
|
+
|
|
35
|
+
| Event | Fields | Why it matters |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `request` | `requestId`, `status`, `durationMs`; plus `method` and `route` with `--request-log detailed` | Error rate and latency per route. `route` is the configured pattern such as `/u/{id}`, never the requested path. |
|
|
38
|
+
| `reload` | `status` (`ok`/`rejected`); `version` and `routes` on `ok` | A `rejected` reload means the last-good snapshot is still serving and a deploy did not take effect. |
|
|
39
|
+
| `watch` | `status` | Development watcher failure; not used by `serve`. |
|
|
40
|
+
| `function_worker` | `status` (`started`/`restarting`), `slot`; `attempt` and `delayMs` on `restarting` | Sustained `restarting` means a function is failing on real traffic. |
|
|
41
|
+
| `link_store_worker` | `status`, `readOnly`, `attempt`, `delayMs` | The same signal for link-store connections. `status: "restarting"` reports an automatic replacement with its backoff; sustained restarts mean the underlying fault is not recoverable. |
|
|
42
|
+
| `link_observer` | `status` (`failed`/`dropped`/`closed`); `reason` on `failed`; `dropped` on `dropped`; the delivery totals on `closed` | Only when an operator enables `linkEvents`. The link event channel below could not keep up or its collector failed. `dropped` means click records were discarded; like `logs_dropped`, anything built on that channel is incomplete while it fires. |
|
|
43
|
+
| `logs_dropped` | `count` | The logger shed records because the collector fell behind. Every other signal is unreliable while this fires. |
|
|
44
|
+
| `observer` | `status` (`failed`), `name` | An in-process observer threw; the request was unaffected. Written to the log only, never to observers. Sustained failures mean the observer's own sink is broken. |
|
|
45
|
+
| `throttle`, `agents`, `cache` | `route`, `outcome`; `remaining` or `list` | Policy decisions; see [policies](POLICIES.md). `throttle` logs `allowed` only in report mode. |
|
|
46
|
+
| `site` | `key`, `path`, `status` (`generated`/`shadowed`); or `severity` (`info`/`warning`) and `message` | Activation records for [site conventions](SITE.md). `shadowed` means a declared route took the path; an `info`/`warning` line reports an omitted `Sitemap:` line (no `--origin`), skipped list names or a far-future `security.txt` expiry. |
|
|
47
|
+
| `management_request` | `timestamp`, `requestId`, `collection`, `action`, `authenticated`, `principal`, `status`, `outcome`, `durationMs` | Operator activity on the link-management API. `status` 0 means no response headers were sent before the peer disconnected; such a request may still have committed a mutation. |
|
|
48
|
+
|
|
49
|
+
### The link event channel
|
|
50
|
+
|
|
51
|
+
`link_request` is **not** a stdout record. It is delivered to an `observe()`
|
|
52
|
+
function the embedding operator process supplies, after the response is over, so
|
|
53
|
+
it can never change, delay or fail a redirect. It carries `requestId`,
|
|
54
|
+
`collection`, `route`, `method`, `status`, `outcome`
|
|
55
|
+
(`completed`/`aborted`/`missing`/`disabled`/`expired`/`invalid_code`/`invalid_record`/`unavailable`)
|
|
56
|
+
and `durationMs`. The short code is redacted unless `includeCode` is set, because
|
|
57
|
+
a code identifies the link somebody followed.
|
|
58
|
+
|
|
59
|
+
The queue is bounded: under overload it drops events and reports the count
|
|
60
|
+
through `link_observer` rather than growing memory. Alert on those drops if you
|
|
61
|
+
count clicks — a quiet channel and a dropping channel look identical downstream.
|
|
62
|
+
|
|
63
|
+
Startup prints `listening` with the effective `origin`, which is what functions
|
|
64
|
+
and absolute URLs see. Behind a proxy or tunnel this must be your public origin;
|
|
65
|
+
forwarded headers are deliberately not trusted. See [tunnels](TUNNELS.md).
|
|
66
|
+
|
|
67
|
+
## Metrics
|
|
68
|
+
|
|
69
|
+
`startServer({ metrics: true })` serves `GET /_urlcode/metrics` in Prometheus
|
|
70
|
+
text format: requests by status class and by configured route, in-flight
|
|
71
|
+
gauges, shed 503s, reloads, worker restarts and healthy slots, policy
|
|
72
|
+
outcomes, link outcomes, dropped logs and observer errors, all prefixed
|
|
73
|
+
`urlcode_`. The same numbers are available in process as `app.metrics()`. The
|
|
74
|
+
endpoint shares the probes' admission budget and bind host and is off by
|
|
75
|
+
default; it discloses route patterns and traffic shape, so keep it internal
|
|
76
|
+
like the probes. [`examples/monitoring/prometheus-scrape.yaml`](../examples/monitoring/prometheus-scrape.yaml)
|
|
77
|
+
scrapes it directly, without a log pipeline. Field names and label sets are
|
|
78
|
+
fixed in [observability](OBSERVABILITY.md).
|
|
79
|
+
|
|
80
|
+
If you would rather keep everything in one process, an observer passed as
|
|
81
|
+
`startServer({ observers })` receives every log record and a periodic metrics
|
|
82
|
+
snapshot; the same page shows an OpenTelemetry sketch.
|
|
83
|
+
|
|
84
|
+
## What to alert on
|
|
85
|
+
|
|
86
|
+
The example rules in
|
|
87
|
+
[`examples/monitoring/prometheus-rules.yaml`](../examples/monitoring/prometheus-rules.yaml)
|
|
88
|
+
cover:
|
|
89
|
+
|
|
90
|
+
- **`UrlcodeDown`** — liveness probe failing. Process-level; check the supervisor.
|
|
91
|
+
- **`UrlcodeNotReady`** — readiness failing for more than a few minutes.
|
|
92
|
+
- **`UrlcodeServerErrors`** — over 5% 5xx on a route. 502 is a function error,
|
|
93
|
+
503 is capacity, 504 is a deadline.
|
|
94
|
+
- **`UrlcodeCapacityShedding`** — sustained 503. Raise `--max-in-flight` or
|
|
95
|
+
`--workers`, or find what is occupying the pool.
|
|
96
|
+
- **`UrlcodeWorkerRestartLoop`** — replacement backs off but never stops, so a
|
|
97
|
+
persistent cause appears as a steady restart rate rather than a stopped pool.
|
|
98
|
+
This is the signal that a function is exceeding its deadline on real traffic.
|
|
99
|
+
- **`UrlcodeLogsDropped`** — the collector is behind, so the other rules are
|
|
100
|
+
blind until it recovers.
|
|
101
|
+
|
|
102
|
+
Pick service objectives for your own application; these thresholds are a
|
|
103
|
+
starting point, not a recommendation for your workload.
|
|
104
|
+
|
|
105
|
+
## Wiring it up
|
|
106
|
+
|
|
107
|
+
1. Send the process's stdout to a collector. The runtime never writes log files
|
|
108
|
+
and owns no rotation or retention; that belongs to the collector.
|
|
109
|
+
2. Get counters either by scraping `/_urlcode/metrics` (enable `metrics`
|
|
110
|
+
and load [`examples/monitoring/prometheus-scrape.yaml`](../examples/monitoring/prometheus-scrape.yaml))
|
|
111
|
+
or by deriving them from the JSON records — see
|
|
112
|
+
[`examples/monitoring/vector.toml`](../examples/monitoring/vector.toml), which
|
|
113
|
+
produces `urlcode_requests_total`, `urlcode_worker_restarts_total` and
|
|
114
|
+
`urlcode_logs_dropped_total`. Fluent Bit, Promtail and Alloy work equally
|
|
115
|
+
well; the field names are what matter. The endpoint labels requests by
|
|
116
|
+
`status_class` where the log pipeline keeps the exact `status`; the example
|
|
117
|
+
rules carry both forms.
|
|
118
|
+
3. Probe both endpoints with blackbox_exporter — see
|
|
119
|
+
[`examples/monitoring/blackbox-jobs.yaml`](../examples/monitoring/blackbox-jobs.yaml).
|
|
120
|
+
4. Load the alert rules and set the thresholds to your objectives.
|
|
121
|
+
|
|
122
|
+
Latency percentiles need a histogram; `durationMs` is per record, so have the
|
|
123
|
+
log pipeline bucket it rather than averaging in the alert.
|
|
124
|
+
|
|
125
|
+
## What this does not give you
|
|
126
|
+
|
|
127
|
+
Dashboards here describe one process. There is no built-in tracing, no
|
|
128
|
+
per-URL analytics, no distributed aggregation, no metrics persistence across
|
|
129
|
+
restarts and no automatic capacity management. The example configuration is a starting point that has not been
|
|
130
|
+
run against a production workload; validate it in your own environment before
|
|
131
|
+
relying on it, and run the drills in
|
|
132
|
+
[release readiness](RELEASE-READINESS.md) before treating any of it as proof.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Observability
|
|
2
|
+
|
|
3
|
+
URLCode reports what it does through three interfaces built on one stream of
|
|
4
|
+
events: the JSON log on stdout, operator **observers** that receive the same
|
|
5
|
+
events in process, and a **metrics snapshot** of counters derived from them,
|
|
6
|
+
optionally served in Prometheus format. Observers mirror [host plugins](PLUGINS.md):
|
|
7
|
+
they are JavaScript an operator passes to `startServer` or `createRuntime`,
|
|
8
|
+
never something a project's YAML can name, and they run with the host's
|
|
9
|
+
privileges. [Monitoring](MONITORING.md) is the operator's guide to probes,
|
|
10
|
+
recipes and alerts; this page is the contract.
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { startServer } from '@jimhoyd/urlcode';
|
|
14
|
+
|
|
15
|
+
await startServer({
|
|
16
|
+
project: './site',
|
|
17
|
+
observers: [myObserver], // in-process event and metrics sinks
|
|
18
|
+
metrics: true, // GET /_urlcode/metrics, Prometheus text format
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Event catalogue
|
|
23
|
+
|
|
24
|
+
`events` in `@jimhoyd/urlcode/observability` is a frozen object mapping each event name
|
|
25
|
+
to the complete list of fields it may carry. A test runs a real server and
|
|
26
|
+
holds every record to it, so a field or event that is not in the table below
|
|
27
|
+
does not ship. Fields marked *optional* are present only in the situations
|
|
28
|
+
named.
|
|
29
|
+
|
|
30
|
+
| Event | Fields | Emitted when |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `request` | `requestId` string, `status` integer, `durationMs` number; `method` string and `route` string or `null` with `--request-log detailed` | Every response the server wrote, including probes and shed 503s. `route` is the configured pattern (`/u/{id}`) or the probe path, never the requested path. |
|
|
33
|
+
| `reload` | `status` `ok`/`rejected`; `version` string and `routes` integer on `ok` | `app.reload()` or the development watcher swapped, or refused to swap, the snapshot. |
|
|
34
|
+
| `watch` | `status` `failed` | The development watcher could not fingerprint the project. |
|
|
35
|
+
| `function_worker` | `status` `started`/`restarting`, `slot` integer; `attempt` and `delayMs` integers on `restarting` | A function worker became ready or is scheduled for replacement. |
|
|
36
|
+
| `link_store_worker` | `status` `started`/`restarting`, `readOnly` boolean; `attempt` and `delayMs` on `restarting` | The same for a link-store connection. |
|
|
37
|
+
| `link_observer` | `status` `failed`/`dropped`/`closed`; `reason` `timeout`/`error` on `failed`; `dropped` integer on `dropped`; `queued`, `delivered`, `dropped`, `failed`, `timedOut`, `closed` on `closed` | Only with `linkEvents`: the link collector failed, the queue shed events, or the channel drained at shutdown. |
|
|
38
|
+
| `link_request` | `requestId`, `collection`, `route`, `code` (only with `includeCode`), `method`, `status`, `outcome`, `durationMs` | Delivered to the `linkEvents.observe` function only, after the response is over. It never reaches the log or observers; its `outcome` is counted in the metrics. |
|
|
39
|
+
| `logs_dropped` | `count` integer | The JSON logger shed records because stdout was not writable. Written by the logger itself, so observers do not see it. |
|
|
40
|
+
| `observer` | `status` `failed`, `name` string | An observer hook threw or rejected. Written to the default log only, never to observers. |
|
|
41
|
+
| `throttle` | `route`, `outcome` `allowed`/`exceeded`, `remaining` integer | A throttle decision. `allowed` is logged only in `mode: report`; enforce mode logs refusals. |
|
|
42
|
+
| `agents` | `route`, `list` string, `outcome` `denied`/`reported` | A User-Agent matched a list. The list name is logged, never the header. |
|
|
43
|
+
| `cache` | `route`, `outcome` `hit`/`stale`/`miss`/`store` | A cache lookup or store. |
|
|
44
|
+
| `management_request` | `timestamp`, `requestId`, `collection`, `action`, `authenticated`, `principal`, `status`, `outcome`, `durationMs` | The link-management API (`startLinkApi`), which has its own logger; `principal` is the operator-configured principal id. |
|
|
45
|
+
| `listening` | `address`, `port`, `mode`, `origin` | Printed once by the CLI at startup, not emitted by the server. |
|
|
46
|
+
|
|
47
|
+
Every event carries `event` (its name). Numbers are JSON numbers, never
|
|
48
|
+
strings.
|
|
49
|
+
|
|
50
|
+
### Privacy guarantees
|
|
51
|
+
|
|
52
|
+
No event, snapshot or exposition carries a request URL, path, query string,
|
|
53
|
+
header, body, client address, User-Agent string, binding, secret, stored link
|
|
54
|
+
destination or user exception text. `route` is always a configured pattern
|
|
55
|
+
from reviewed YAML. `requestId` is server-generated unless
|
|
56
|
+
`--trust-request-id` accepts one from a trusted proxy. `code` in
|
|
57
|
+
`link_request` is redacted unless the operator sets `includeCode`. An observer
|
|
58
|
+
that logs should keep the same rule; nothing in an event lets it break it.
|
|
59
|
+
|
|
60
|
+
## Observers
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const myObserver = {
|
|
64
|
+
name: 'forwarder', // ^[a-z][a-z0-9-]{0,63}$, unique per server
|
|
65
|
+
version: '1.0.0', // any string up to 64 characters
|
|
66
|
+
onEvent(event) {}, // every record the JSON logger writes, in order
|
|
67
|
+
onMetrics(snapshot) {}, // the metrics snapshot, on the interval and at close
|
|
68
|
+
async onClose() {}, // release resources; reverse order
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
In TypeScript the contract is `Observer` from `@jimhoyd/urlcode/observability` (also
|
|
73
|
+
exported from `urlcode`), with `ObserverEvent` for a record and
|
|
74
|
+
`MetricsSnapshot` for what `onMetrics` receives; the declarations ship with the
|
|
75
|
+
package:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import type { Observer, ObserverEvent, MetricsSnapshot } from '@jimhoyd/urlcode/observability';
|
|
79
|
+
|
|
80
|
+
const myObserver: Observer = {
|
|
81
|
+
name: 'forwarder',
|
|
82
|
+
version: '1.0.0',
|
|
83
|
+
onEvent(event: ObserverEvent) { queue.push(event); },
|
|
84
|
+
onMetrics(snapshot: MetricsSnapshot) { gauge.set(snapshot.requests.inFlight); },
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Validation (`validateObservers`) matches plugins: at most 32 observers, each
|
|
89
|
+
an object with a kebab-case `name` no other observer uses, a `version` string,
|
|
90
|
+
every declared hook a function and at least one present. It runs before the
|
|
91
|
+
listener starts, so a bad observer fails startup rather than a request.
|
|
92
|
+
|
|
93
|
+
`onEvent` receives the same object the logger serialised, after the logger,
|
|
94
|
+
observers in array order. Do not mutate it. It runs on the request path, so
|
|
95
|
+
keep it cheap: buffer and flush on a timer rather than awaiting a network
|
|
96
|
+
call. A hook that throws or returns a rejecting promise is isolated: the
|
|
97
|
+
request is unaffected, the next observer still runs, `observers.errors` in
|
|
98
|
+
the snapshot increments and one `observer` record goes to the default log.
|
|
99
|
+
Nothing is retried; an observer that needs delivery guarantees owns its own
|
|
100
|
+
queue, like the [link event channel](MONITORING.md#the-link-event-channel).
|
|
101
|
+
|
|
102
|
+
`onMetrics` receives a fresh snapshot every `metricsIntervalMs`
|
|
103
|
+
(`startServer` option, `0` off by default, 1 s to 1 h) and once at `close()`.
|
|
104
|
+
`onClose` runs in reverse order after the runtime and the link channel have
|
|
105
|
+
drained. `app.observers` lists the `{ name, version }` pairs.
|
|
106
|
+
|
|
107
|
+
`createRuntime(project, { observers })` takes the same array for embedding
|
|
108
|
+
without the server: the runtime's own sink and counters are then yours, and
|
|
109
|
+
`runtime.metrics()` returns its snapshot. `startServer` never passes its
|
|
110
|
+
observers down to the runtimes it creates, so a reload does not re-register
|
|
111
|
+
them and counters survive reloads.
|
|
112
|
+
|
|
113
|
+
`createObserverSink(observers, fallbackLog)` is the fan-out itself, exported
|
|
114
|
+
for tests and custom hosts: it returns a `log(event)` function with `.metrics`,
|
|
115
|
+
`.publish(snapshot)` and `.close()`.
|
|
116
|
+
|
|
117
|
+
## Metrics snapshot
|
|
118
|
+
|
|
119
|
+
`app.metrics()` and `runtime.metrics()` return a plain object, safe to
|
|
120
|
+
`JSON.stringify`, of counters since the process started serving. Numbers
|
|
121
|
+
only; the one keyed table is `requests.byRoute`, keyed by configured pattern
|
|
122
|
+
and capped at 10 000 keys.
|
|
123
|
+
|
|
124
|
+
| Field | Type | Meaning |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `version` | gauge | Snapshot shape version, currently `1`. |
|
|
127
|
+
| `uptimeSeconds`, `rssBytes` | gauge | Process facts. |
|
|
128
|
+
| `requests.total`, `requests.byStatusClass.{2xx,3xx,4xx,5xx}` | counter | Application responses, including shed 503s. |
|
|
129
|
+
| `requests.inFlight` | gauge | Requests holding application admission now. |
|
|
130
|
+
| `requests.byRoute[pattern]` | counter | Responses per matched route. A shed or unmatched request has no route. |
|
|
131
|
+
| `health.total`, `health.byStatusClass`, `health.inFlight` | counter, gauge | The probe budget: `/_urlcode/health`, `/_urlcode/ready` and `/_urlcode/metrics`. |
|
|
132
|
+
| `shed.requests`, `shed.health` | counter | 503s answered because an admission budget was full. |
|
|
133
|
+
| `reloads.ok`, `reloads.rejected` | counter | Snapshot swaps. |
|
|
134
|
+
| `watch.failed` | counter | Development watcher failures. |
|
|
135
|
+
| `functionWorkers.started`, `functionWorkers.restarts` | counter | Worker starts and scheduled replacements. |
|
|
136
|
+
| `functionWorkers.healthySlots`, `functionWorkers.slots` | gauge | Ready slots and configured slots of the serving runtime. |
|
|
137
|
+
| `linkStoreWorkers.started`, `linkStoreWorkers.restarts` | counter | The same for link-store connections. |
|
|
138
|
+
| `policies.throttle.{allowed,exceeded}` | counter | Throttle decisions (see the catalogue for what enforce mode logs). |
|
|
139
|
+
| `policies.agents.{denied,reported}` | counter | Agents decisions. |
|
|
140
|
+
| `policies.cache.{hit,stale,miss,store}` | counter | Cache outcomes. |
|
|
141
|
+
| `linkRequests.{completed,aborted,missing,disabled,expired,invalid_code,invalid_record,unavailable}` | counter | Dynamic link outcomes, counted whether or not `linkEvents` is set. |
|
|
142
|
+
| `linkObserver.failed`, `linkObserver.dropped` | counter | Link collector failures and dropped events. |
|
|
143
|
+
| `logsDropped` | counter | Records the JSON logger shed. |
|
|
144
|
+
| `observers.errors` | counter | Observer hooks that threw or rejected. |
|
|
145
|
+
|
|
146
|
+
Policy counters are derived from the `throttle`, `agents` and `cache` events
|
|
147
|
+
as they pass through the sink, so the policies themselves have no metrics
|
|
148
|
+
code. Runtime facts that never become events (admission, shedding, slot
|
|
149
|
+
health) are recorded by the server directly. Counters are per process;
|
|
150
|
+
aggregation across replicas is the scraper's job.
|
|
151
|
+
|
|
152
|
+
## Prometheus exposition
|
|
153
|
+
|
|
154
|
+
`startServer({ metrics: true })` serves `GET /_urlcode/metrics` as
|
|
155
|
+
`text/plain; version=0.0.4`, rendered from the same snapshot by
|
|
156
|
+
`renderPrometheus(snapshot)`, a pure function you can also call yourself.
|
|
157
|
+
Every metric is prefixed `urlcode_`; counters end in `_total`; the only labels
|
|
158
|
+
are `status_class`, `route` and `outcome`.
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
# HELP urlcode_requests_total Application requests answered since start, by status class.
|
|
162
|
+
# TYPE urlcode_requests_total counter
|
|
163
|
+
urlcode_requests_total{status_class="2xx"} 1042
|
|
164
|
+
urlcode_route_requests_total{route="/u/{id}"} 977
|
|
165
|
+
urlcode_requests_in_flight 3
|
|
166
|
+
urlcode_shed_total{outcome="requests"} 0
|
|
167
|
+
urlcode_reloads_total{outcome="ok"} 2
|
|
168
|
+
urlcode_function_worker_restarts_total 0
|
|
169
|
+
urlcode_function_worker_healthy_slots 2
|
|
170
|
+
urlcode_throttle_total{outcome="exceeded"} 14
|
|
171
|
+
urlcode_cache_total{outcome="hit"} 511
|
|
172
|
+
urlcode_link_requests_total{outcome="completed"} 0
|
|
173
|
+
urlcode_logs_dropped_total 0
|
|
174
|
+
urlcode_observer_errors_total 0
|
|
175
|
+
urlcode_uptime_seconds 86400
|
|
176
|
+
urlcode_process_rss_bytes 71303168
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The endpoint is **off by default**. It shares the probes' admission budget
|
|
180
|
+
(`--max-in-flight-health`) and the same bind host, which is `127.0.0.1`
|
|
181
|
+
unless `--host` says otherwise. Like the probes it is unauthenticated and
|
|
182
|
+
discloses route patterns and traffic shape, so **do not expose it publicly**:
|
|
183
|
+
keep it on an internal interface or restrict it at the ingress. A scrape
|
|
184
|
+
counts under `health`, not under application requests. See
|
|
185
|
+
[`examples/monitoring/prometheus-scrape.yaml`](../examples/monitoring/prometheus-scrape.yaml).
|
|
186
|
+
|
|
187
|
+
## OpenTelemetry sketch
|
|
188
|
+
|
|
189
|
+
The runtime has no OpenTelemetry dependency. An observer can forward events
|
|
190
|
+
to an OTLP exporter and map the snapshot onto instruments; this is a sketch,
|
|
191
|
+
not shipped code, and omits batching, resource attributes and error handling.
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
// Sketch. `logs` and `meter` come from the OpenTelemetry SDK the operator
|
|
195
|
+
// configures; the runtime knows nothing about them.
|
|
196
|
+
export function otelObserver({ logger, meter }) {
|
|
197
|
+
const requests = meter.createCounter('urlcode.requests', { unit: '{request}' });
|
|
198
|
+
const inFlight = meter.createObservableGauge('urlcode.requests.in_flight');
|
|
199
|
+
let last;
|
|
200
|
+
inFlight.addCallback(result => { if (last) result.observe(last.requests.inFlight); });
|
|
201
|
+
return {
|
|
202
|
+
name: 'otel', version: '0.1.0',
|
|
203
|
+
onEvent(event) {
|
|
204
|
+
// Every field is already safe to attach as an attribute.
|
|
205
|
+
logger.emit({ body: event.event, attributes: event });
|
|
206
|
+
if (event.event === 'request') requests.add(1, { status_class: `${Math.floor(event.status / 100)}xx`, route: event.route ?? '' });
|
|
207
|
+
},
|
|
208
|
+
onMetrics(snapshot) { last = snapshot; }, // the gauge reads the latest snapshot
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Counters in the snapshot are cumulative, so they map to OpenTelemetry
|
|
214
|
+
`Counter` instruments read through an observable callback, or to a
|
|
215
|
+
Prometheus receiver scraping `/_urlcode/metrics` directly. `durationMs` on
|
|
216
|
+
`request` is the input for a `Histogram`; the runtime does not bucket it.
|
|
217
|
+
|
|
218
|
+
## What is not provided
|
|
219
|
+
|
|
220
|
+
- **Tracing.** There are no spans and no context propagation; `requestId` is
|
|
221
|
+
the only correlation key, and a trusted proxy can supply it.
|
|
222
|
+
- **Sampling.** Every event is delivered to every observer, or shed by the
|
|
223
|
+
logger under back-pressure and reported as `logs_dropped`.
|
|
224
|
+
- **Persistence and aggregation.** Counters live in process memory, reset on
|
|
225
|
+
restart, and describe one process. Retention and cross-replica sums belong
|
|
226
|
+
to the collector.
|
|
227
|
+
- **Per-URL analytics.** By design; see the privacy guarantees.
|
|
228
|
+
- **Adapters.** Vercel, Lambda and Cloudflare handlers emit through the
|
|
229
|
+
platform's own logging and do not take observers.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Operational evidence and deployment acceptance
|
|
2
|
+
|
|
3
|
+
`node scripts/operational-drills.ts` creates and deletes its own temporary project
|
|
4
|
+
and store. It runs mixed real HTTP requests against native redirects, isolated
|
|
5
|
+
functions and live links while updating records; asserts responses; rejects a bad
|
|
6
|
+
reload; activates and rolls back a configuration; then closes all users of the
|
|
7
|
+
store, explicitly verifies a successful WAL checkpoint, and restores a copied database. It checks integrity, record/version and
|
|
8
|
+
latest durable audit revision. `URLCODE_SOAK_SECONDS=60` selects a longer run
|
|
9
|
+
(1–3600 seconds, default 5). Output is JSON with request count, batch p99 duration,
|
|
10
|
+
RSS and restore time. Batch latency is not per-request p99 or a capacity promise.
|
|
11
|
+
CI runs the short drill on all six supported Node/OS combinations.
|
|
12
|
+
|
|
13
|
+
The container job additionally uses `--disk-full-dir /state` on a disposable
|
|
14
|
+
16 MiB tmpfs. It reserves recovery space, writes until a real volume exhaustion
|
|
15
|
+
failure, checks available disk space, releases the reserve, proves a subsequent
|
|
16
|
+
mutation succeeds, and checks integrity and equal committed link/audit counts.
|
|
17
|
+
Never point this option at production storage: it intentionally consumes up to
|
|
18
|
+
84 MiB of writes in a newly created temporary child directory. The CI mount is
|
|
19
|
+
nonpersistent. An I/O device failure or power cut is a different failure mode.
|
|
20
|
+
|
|
21
|
+
## Required proof on the intended deployment
|
|
22
|
+
|
|
23
|
+
Local/CI passes do not close these gates. The deployment owner must record:
|
|
24
|
+
|
|
25
|
+
| Exercise | Acceptance evidence |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Soak | At least 24 hours at expected peak and burst load through actual TLS/ingress; native/function/live-link mix, slow clients, response correctness, p50/p95/p99, throughput, error/rejection rate, RSS plateau, CPU, FD and disk/WAL growth. Define numerical SLOs before starting |
|
|
28
|
+
| Restore | Restore a consistent backup onto a separate host/volume. Reconcile collection counts, latest committed revisions and audit journal; measure RPO/RTO against agreed targets. Test encrypted backup access and credential recovery |
|
|
29
|
+
| Logical export/restore | Run `links export` against a store under write load, restore it with `links import` onto a separate store, and reconcile record counts and every field. Prove the export's digest verifies, that a truncated stream is rejected, and that stale management ETags are discarded because the restore reassigns versions. An export carries no audit journal, so pair it with a file backup |
|
|
30
|
+
| Kill/restart | Kill server and writer with outstanding reads/writes. Reconcile uncertain mutations by version/request ID, run integrity checks, restore readiness, and prove no duplicate successful conditional writes |
|
|
31
|
+
| Disk/log exhaustion | Fill the actual disposable staging storage type and log destination. Verify bounded 503s, audit/mutation atomicity, alert delivery through an independent sink, reserve-space recovery and readiness after recovery |
|
|
32
|
+
| Rollback | Deploy candidate by exact digest beside last-good, run route assertions, switch ingress, drain, then switch back. Verify configuration/policy compatibility and audit continuity; never downgrade to an unaudited writer |
|
|
33
|
+
| Monitoring | Deliver test alerts for missing logs, sustained errors, pool rejection/failure, low disk, restart storms, backup age and failed readiness to a named on-call owner |
|
|
34
|
+
|
|
35
|
+
Record date, operator, source/app/policy/image digests, topology, hardware/limits,
|
|
36
|
+
commands, duration, synthetic dataset size, raw metrics/log locations, result,
|
|
37
|
+
RPO/RTO and unresolved findings. A reviewer signs the acceptance record; a blank
|
|
38
|
+
record is not a pass. Do not copy only the main file of a live WAL database.
|
|
39
|
+
The executable local drill uses a fully quiesced, explicitly checkpointed database intentionally. A last
|
|
40
|
+
read-only connection can leave WAL frames even after every connection closes;
|
|
41
|
+
closing alone is insufficient proof that the main database file is a full backup. For a
|
|
42
|
+
live-backup system, prove its SQLite-consistent snapshot method separately.
|