§9 structured logging + OTLP observability (todo §9); structured, OTLP-native logging on @larvit/log (2.3.0, pinned; itself zero-dependency — the one new runtime dep). New pure src/logger.ts: createLogger() builds one app Log tagged service.name=plainpages (level/format/OTLP from config, injectable stdout/stderr); requestLogger() clones it per request (own root trace, inheriting level/format/streams/OTLP) into a "request" span, adopting an inbound W3C traceparent so a request continues an upstream proxy's distributed trace (malformed ⇒ fresh trace; clone honours a passed traceparent while dropping the parent's, unlike parentLog). app.ts builds the per-request log at the top of the handler and on res "close" (fires on completion AND abort, unlike "finish") emits one access line (method/path-without-query/status/ms/requestId, guarded) then end()s to flush the span (fire-and-forget .catch — a flaky collector never crashes a served request); the catch-all 500 + Ory-unreachable re-mint now log via reqLog.error/warn; static.ts mid-stream error takes an injected onError. server.ts builds the app logger, logs discovery/listen/shutdown, end()-flushes on SIGTERM/SIGINT (re-entry-guarded). bootstrap.ts events go structured (the human first-run banner stays raw). Config (environment-agnostic, fail-loud): LOG_LEVEL (info), LOG_FORMAT (text; prod compose → json), OTLP_ENDPOINT (unset ⇒ console-only; set ⇒ export logs + spans to an OTel Collector), OTLP_PROTOCOL (http/json|http/protobuf). compose: base sets LOG_FORMAT=json, dev override flips it to text. Tests-first: logger.test.ts (service.name/severity/level-gate/format, OTLP-only-when-endpoint, a stubbed-fetch proof it POSTs /v1/logs, requestLogger context-merge/own-root-trace/traceparent-continue/malformed-ignored), config.test.ts (4 toggles + validation), app.test.ts (live request emits the JSON access line), compose.test.ts (prod json / dev text). Stability-reviewer: APPROVE, no Critical/High (addressed both yellow nits — guarded access line + "finish"→"close" so aborted requests log; shutdown re-entry guard — and the green ones). README (config table, new Observability section, Status, Layout, runtime-deps) + AGENTS (deps) updated. typecheck + 326 units green (317 → 326).
This commit is contained in:
@@ -10,6 +10,7 @@ import { existsSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { discoverPlugins } from "./discovery.ts";
|
||||
import { generateJwks, type JwkSet } from "./gen-jwks.ts";
|
||||
import { createLogger } from "./logger.ts";
|
||||
|
||||
// --- Pure payload builders (the Kratos/Keto request contracts) -----------------------
|
||||
|
||||
@@ -116,7 +117,7 @@ async function findIdentityId(http: typeof fetch, adminUrl: string, email: strin
|
||||
// --- First-run banner ----------------------------------------------------------------
|
||||
|
||||
// Loud, scannable block in the compose logs: where to log in + the seeded demo creds +
|
||||
// the "change before production" warning. Pure so it's testable; main() console.logs it.
|
||||
// the "change before production" warning. Pure so it's testable; main() prints it verbatim.
|
||||
export function firstRunBanner(opts: { appUrl: string; email: string; password: string }): string {
|
||||
const rule = "─".repeat(58);
|
||||
return [
|
||||
@@ -133,7 +134,9 @@ export function firstRunBanner(opts: { appUrl: string; email: string; password:
|
||||
|
||||
async function main() {
|
||||
const env = process.env;
|
||||
if (ensureJwks(env["JWKS_FILE"] ?? "/etc/config/kratos/tokenizer/jwks.json")) console.log("bootstrap: generated a JWKS signing key");
|
||||
// Structured like the web app (§9) so prod logs stay uniform; honour LOG_FORMAT, default text.
|
||||
const log = createLogger({ format: env["LOG_FORMAT"] === "json" ? "json" : "text" });
|
||||
if (ensureJwks(env["JWKS_FILE"] ?? "/etc/config/kratos/tokenizer/jwks.json")) log.info("generated a JWKS signing key");
|
||||
|
||||
// Seed `admin` (or ADMIN_ROLES) + every discovered plugin's declared permission tokens, so the
|
||||
// shipped example — and any dropped-in plugin — works for the demo admin without a host edit.
|
||||
@@ -148,7 +151,8 @@ async function main() {
|
||||
password,
|
||||
roles,
|
||||
});
|
||||
console.log(`bootstrap: admin ${result.created ? "created" : "already present"} (${result.id}); roles granted: ${result.roles.join(", ")}`);
|
||||
log.info("admin seeded", { created: result.created, id: result.id, roles: result.roles.join(", ") });
|
||||
// The banner is human-facing UX (the first-run "you're ready" block), not a log event — print raw.
|
||||
console.log(firstRunBanner({ appUrl: env["APP_URL"] ?? "http://localhost:3000", email, password }));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user