feat(api): 访问日志与 HTTP/运行时指标、安全响应头与面板 CSP、跨站写拦截、请求体读截止、SSE 定期重鉴权与续传、404/405/413 信封、status 按所有权裁剪、停机并发排空

This commit is contained in:
Lemon-miaow committed 2026-09-25 02:14:08 +08:00
1 parent cc85aac906
commit f8f112b8ca
24 files changed
+1453 -55

No files matched your search

+44 -4
View File
@@ -31,6 +31,28 @@ info:
and the panel). Admin-tier external operations additionally require the admin
Access path. See `x-felis-face` / `x-felis-tier` on each operation.
Behaviour every operation shares, and so not repeated under each:
* Every response carries `X-Request-Id` (a well-formed inbound one is kept),
`X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`,
`Referrer-Policy: no-referrer` and `Content-Security-Policy: default-src 'none'`.
`Strict-Transport-Security` is added when the request came through the TLS
edge (`X-Forwarded-Proto: https`).
* A path no operation serves is `404 not_found`; a path served under other
methods is `405 method_not_allowed` with an `Allow` header.
* A POST/PUT/PATCH/DELETE a browser sends from another site (`Sec-Fetch-Site`
`same-site` or `cross-site`, or an `Origin` whose host is not the request's)
is `403 cross_site`, before authentication. Callers that send neither header
(the plugins, scripts) are unaffected.
* A JSON body over 1 MiB is `413 too_large`. A request body must keep arriving:
after 30 s it has to average 16 KiB/s or the connection is closed.
* Event streams (the console and build logs) tag each line with `id:` (unix
seconds); an EventSource that reconnects with `Last-Event-ID` within the hour
resumes from that second instead of the tailed backlog. The server re-checks
the caller every minute and ends the stream with `event: revoked` once the
session or the access is gone; a stream also closes after 30 minutes and on
server shutdown, and the client simply reconnects.
servers:
- url: https://api-internal.{root_domain}
description: >-
@@ -1443,9 +1465,16 @@ paths:
security: [{ accessJWT: [] }]
parameters:
- { name: name, in: path, required: true, schema: { type: string } }
- name: Last-Event-ID
in: header
required: false
description: The id of the last line received; resumes the stream from that second (within the hour).
schema: { type: string }
responses:
'200':
description: An event stream of log lines.
description: >-
An event stream of log lines (`id:` + `data:` per line, `:` comments as
keep-alives), ended by `event: revoked` when access is withdrawn.
content:
text/event-stream:
schema: { type: string }
@@ -1871,7 +1900,11 @@ paths:
get:
tags: [servers]
operationId: status
summary: Status of your own server.
summary: Status of a server; the full record for its owner and staff.
description: >-
Anyone signed in may ask. The owner and staff get the whole projection;
anyone else gets what the game's own server list shows: name, subdomain,
displayName, phase, ready, playersOnline and playersMax.
x-felis-face: [external]
x-felis-tier: app
security: [{ accessJWT: [] }]
@@ -1879,7 +1912,7 @@ paths:
- { name: name, in: path, required: true, schema: { type: string } }
responses:
'200':
description: The server's status projection.
description: The server's status projection (trimmed for non-owners).
content:
application/json:
schema: { $ref: '#/components/schemas/ServerInfo' }
@@ -4707,9 +4740,16 @@ paths:
security: [{ accessJWT: [] }]
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
- name: Last-Event-ID
in: header
required: false
description: The id of the last line received; resumes the stream from that second (within the hour).
schema: { type: string }
responses:
'200':
description: An event stream of build log lines.
description: >-
An event stream of build log lines (`id:` + `data:` per line), ended by
`event: revoked` when the caller is no longer staff.
content:
text/event-stream:
schema: { type: string }
+22 -1
View File
@@ -1238,6 +1238,26 @@ All four mandated metrics have real producers; scrape them when triaging:
- `felis_reaper_worlds_deleted_total` — increments only after a world PVC is
actually deleted post-backup (§10); a spike here means worlds crossed the 15d
idle line — cross-check that join events are flowing (§10 risk vectors).
- `felis_http_requests_total{face,method,route,code}` and
`felis_http_request_duration_seconds{face,route}` — every API request, by the
route pattern it matched (`/api/v1/servers/{name}/status`, never the raw
path; `unmatched` for a path no route serves). `face` is `internal` or
`external`. Log streams count as requests but stay out of the latency
histogram. A rise in `code=~"5.."` on one route narrows a failure to one
handler; `route="unmatched"` rising is someone scanning.
- The Go runtime and process series (`go_*`, `process_*`) of `felis-api`:
goroutines, heap, open file descriptors. Goroutines that climb without
falling back usually mean streams or uploads that never end.
The API also writes one access-log line per request to its log, in logfmt:
`face`, `method`, `route`, `path`, `status`, `duration_ms`, `bytes`,
`request_id` (the id in every error envelope) and `principal` (the user id,
once signed in). Successful probes and scrapes are left out.
```bash
kubectl -n felis logs deploy/felis-api | grep 'msg=request' | grep 'status=5'
kubectl -n felis logs deploy/felis-api | grep 'request_id=<id from the error>'
```
### Scraping
@@ -1250,7 +1270,8 @@ annotated Service endpoints picks them up as is.
`felis_build_info{component="operator"}`, and controller-runtime's
`controller_runtime_reconcile_*` / `workqueue_*` series.
- `felis-api` internal face `:8081/metrics` (Service `felis-api-internal`) —
`felis_build_info{component="api"}`,
`felis_build_info{component="api"}`, the `felis_http_*` request series, the
`go_*`/`process_*` runtime series,
`felis_image_build_failures_total`, and the sign-in series of §17
(`felis_mail_total`, `felis_rate_limited_total`,
`felis_auth_otp_lockouts_total`, `felis_auth_failures_total`,