From 444044b99a830560a073b016707038a06ccd23c8 Mon Sep 17 00:00:00 2001 From: nholtware Date: Thu, 5 Nov 2026 10:54:12 -0600 Subject: [PATCH] feat: JSON access log in the platform logging contract --- CLAUDE.md | 1 + Dockerfile | 3 +++ README.md | 7 +++++++ docker-entrypoint.d/10-config.sh | 10 +++++++++- nginx/default.conf | 26 ++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index dc73156..5324714 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,5 +5,6 @@ - Routes: table in `src/routes.tsx`, one component per file in `src/routes/`, loaders exported next to their component. - Runtime config: read only via `getConfig()` (`src/lib/config.ts`). A new browser-visible env var must be added to `VARS` in `docker-entrypoint.d/10-config.sh`, `AppConfig`, and the gitops Deployment. No secrets: config.js is public. - Backend calls: `apiGet()` in `src/lib/api.ts`, relative to `API_BASE_URL` (`/api` same-host ingress path, stripped before the backend). No CORS. +- Logging: nginx access log is JSON in the platform logging contract (`log_format contract` in `nginx/default.conf`; `$app_name`/`$git_sha` from `conf.d/service-vars.conf` written by `10-config.sh`). Keep field names identical to the backend template's pino-http lines. - Serving contract: port 8080, `/healthz` 200, SPA fallback, immutable `/assets/`, no-cache `index.html`/`config.js` (`nginx/default.conf`). - Deploy: push to `main` -> Forgejo CI -> image -> gitops bump -> ArgoCD. Never `kubectl apply` by hand. diff --git a/Dockerfile b/Dockerfile index 9ca9be2..5aeda14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,7 @@ COPY nginx/default.conf /etc/nginx/conf.d/default.conf COPY --chmod=0755 docker-entrypoint.d/10-config.sh /docker-entrypoint.d/10-config.sh # html dir owned by nginx so 10-config.sh can write config.js at start. COPY --from=build --chown=101:101 /app/dist /usr/share/nginx/html +# Logged as "version" in every access-log line. +ARG GIT_SHA=dev +ENV GIT_SHA=${GIT_SHA} EXPOSE 8080 diff --git a/README.md b/README.md index a2d8c3a..2417e60 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,13 @@ Everything in `config.js` is public. Never put secrets there. - nginx listens on 8080: `/healthz` returns 200, unknown paths fall back to `index.html`, `/assets/*` are cached as immutable, and `index.html` and `config.js` are not cached. +- Access log: one JSON line per request in the platform logging contract + (`level`, `time`, `service` = `APP_NAME`, `version` = `GIT_SHA`, `req.id` + (echoes `X-Request-Id`), `req.method`, `req.url`, `res.statusCode`, + `responseTime` in ms, `msg: "request completed"`), the same shape as the backend + template. In Kibana (http://kibana.192.168.1.83.nip.io, data view `kube-*`): + `service:"hello-web" and res.statusCode >= 400`. `/healthz` isn't logged. + nginx's own startup/error lines stay plain text (field `log`). ## Deploy diff --git a/docker-entrypoint.d/10-config.sh b/docker-entrypoint.d/10-config.sh index c4f4ee0..2aa90ae 100755 --- a/docker-entrypoint.d/10-config.sh +++ b/docker-entrypoint.d/10-config.sh @@ -23,4 +23,12 @@ for name in $VARS; do done printf 'window.__APP_CONFIG__ = {%s};\n' "$body" > "$OUT" -echo "10-config.sh: wrote $OUT" + +# nginx variables for the JSON access log (nginx/default.conf). Restricted +# charset: these land inside nginx string literals. +safe() { printf '%s' "$1" | tr -cd 'A-Za-z0-9._-'; } +cat > /etc/nginx/conf.d/service-vars.conf <\d+)$" $ms; + "~^(?[1-9]\d*)\.(?\d{3})$" $s$ms; + default 0; +} +log_format contract escape=json + '{"level":"$log_level","time":"$time_iso8601","service":"$app_name",' + '"version":"$git_sha","req":{"id":"$req_id","method":"$request_method",' + '"url":"$request_uri"},"res":{"statusCode":$status},' + '"responseTime":$response_ms,"msg":"request completed"}'; + server { listen 8080; server_name _; root /usr/share/nginx/html; + access_log /dev/stdout contract; # Liveness/readiness for k8s probes. location = /healthz {