From 57348b14ab956b5059273542db88ad607d25ade9 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Tue, 26 May 2026 01:02:26 +0200 Subject: [PATCH] feat: per-request tracing via BSB createTrace Each HTTP request gets a fresh BSB trace (not a child span of init). onRequest creates trace, stores on event.context.obs. onError logs with trace context. onResponse ends the trace. 4xx logged as warn, 5xx as error. H3EventContext typed with obs field. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/plugins/service-admin-http/index.ts | 23 ++++++++++++++++++- .../plugins/service-admin-http/middleware.ts | 1 + server/src/plugins/service-api-http/index.ts | 23 ++++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/server/src/plugins/service-admin-http/index.ts b/server/src/plugins/service-admin-http/index.ts index 04a1326..6f2f757 100644 --- a/server/src/plugins/service-admin-http/index.ts +++ b/server/src/plugins/service-admin-http/index.ts @@ -195,16 +195,37 @@ export class Plugin extends BSBService, typeof Event otaImportApiKey: this.config.otaImportApiKey || undefined, }; + const self = this; const app = new H3({ + onRequest: (event) => { + const method = event.req.method ?? "GET"; + const path = event.req.url ?? "/"; + event.context.obs = self.createTrace(`${method} ${path}`, { + "http.method": method, + "http.url": path, + }); + }, onError: (error, event) => { + const reqObs = event.context.obs ?? obs; const status = error.status ?? 500; const path = event.req.url ?? "unknown"; if (status >= 500) { - obs.log.warn("HTTP {status} {path}: {err}", { + reqObs.log.error("HTTP {status} {path}: {err}", { status, path, err: error.message ?? String(error), }); + } else if (status >= 400) { + reqObs.log.warn("HTTP {status} {path}: {err}", { + status, + path, + err: error.message ?? String(error), + }); + } + }, + onResponse: (_response, event) => { + if (event.context.obs) { + event.context.obs.end(); } }, }); diff --git a/server/src/plugins/service-admin-http/middleware.ts b/server/src/plugins/service-admin-http/middleware.ts index 40f2d96..753f4fb 100644 --- a/server/src/plugins/service-admin-http/middleware.ts +++ b/server/src/plugins/service-admin-http/middleware.ts @@ -16,6 +16,7 @@ declare module "h3" { user?: User; session?: Session; apiKeyPrefix?: string; + obs?: import("@bsb/base").Observable; } } diff --git a/server/src/plugins/service-api-http/index.ts b/server/src/plugins/service-api-http/index.ts index 3525b6e..505eaf1 100644 --- a/server/src/plugins/service-api-http/index.ts +++ b/server/src/plugins/service-api-http/index.ts @@ -164,16 +164,37 @@ export class Plugin extends BSBService, typeof Event }, ); + const self = this; const app = new H3({ + onRequest: (event) => { + const method = event.req.method ?? "GET"; + const path = event.req.url ?? "/"; + event.context.obs = self.createTrace(`${method} ${path}`, { + "http.method": method, + "http.url": path, + }); + }, onError: (error, event) => { + const reqObs = event.context.obs ?? obs; const status = error.status ?? 500; const path = event.req.url ?? "unknown"; if (status >= 500) { - obs.log.warn("HTTP {status} {path}: {err}", { + reqObs.log.error("HTTP {status} {path}: {err}", { status, path, err: error.message ?? String(error), }); + } else if (status >= 400) { + reqObs.log.warn("HTTP {status} {path}: {err}", { + status, + path, + err: error.message ?? String(error), + }); + } + }, + onResponse: (_response, event) => { + if (event.context.obs) { + event.context.obs.end(); } }, });