feat: add request ID propagation from Rust to Bun with structured logging

- Forward x-request-id header through proxy and API calls
- Store RequestId in request extensions for downstream access
- Add AsyncLocalStorage context to correlate logs across async boundaries
- Improve migration logging to show pending changes before applying
- Reduce noise in logs (common OG images, health checks)
This commit is contained in:
2026-01-13 16:42:14 -06:00
parent 6d8766d3a6
commit a6cc0b8e66
13 changed files with 173 additions and 53 deletions
+39 -24
View File
@@ -1,6 +1,7 @@
import type { Handle, HandleServerError } from "@sveltejs/kit";
import { dev } from "$app/environment";
import { initLogger } from "$lib/logger";
import { requestContext } from "$lib/server/context";
import { preCacheCollections } from "$lib/server/icons";
import { getLogger } from "@logtape/logtape";
import { minify } from "html-minifier-terser";
@@ -13,6 +14,18 @@ await preCacheCollections();
const logger = getLogger(["ssr", "error"]);
export const handle: Handle = async ({ event, resolve }) => {
// Extract request ID from Rust proxy (should always be present in production)
const requestId = event.request.headers.get("x-request-id");
if (!requestId) {
const reqLogger = getLogger(["ssr", "request"]);
reqLogger.warn(
"Missing x-request-id header - request not routed through Rust proxy",
{
path: event.url.pathname,
},
);
}
if (
dev &&
event.url.pathname === "/.well-known/appspecific/com.chrome.devtools.json"
@@ -20,31 +33,33 @@ export const handle: Handle = async ({ event, resolve }) => {
return new Response(undefined, { status: 404 });
}
const response = await resolve(event, {
transformPageChunk: !dev
? ({ html }) =>
minify(html, {
collapseBooleanAttributes: true,
collapseWhitespace: true,
conservativeCollapse: true,
decodeEntities: true,
html5: true,
ignoreCustomComments: [/^\[/],
minifyCSS: true,
minifyJS: true,
removeAttributeQuotes: true,
removeComments: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortAttributes: true,
sortClassName: true,
})
: undefined,
});
return requestContext.run({ requestId: requestId ?? undefined }, async () => {
const response = await resolve(event, {
transformPageChunk: !dev
? ({ html }) =>
minify(html, {
collapseBooleanAttributes: true,
collapseWhitespace: true,
conservativeCollapse: true,
decodeEntities: true,
html5: true,
ignoreCustomComments: [/^\[/],
minifyCSS: true,
minifyJS: true,
removeAttributeQuotes: true,
removeComments: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortAttributes: true,
sortClassName: true,
})
: undefined,
});
return response;
return response;
});
};
export const handleError: HandleServerError = async ({
+10
View File
@@ -1,5 +1,6 @@
import { getLogger } from "@logtape/logtape";
import { env } from "$env/dynamic/private";
import { requestContext } from "$lib/server/context";
const logger = getLogger(["ssr", "lib", "api"]);
@@ -39,6 +40,15 @@ function createSmartFetch(upstreamUrl: string) {
// Remove custom fetch property from options (not part of standard RequestInit)
delete (fetchOptions as Record<string, unknown>).fetch;
// Forward request ID to Rust API
const ctx = requestContext.getStore();
if (ctx?.requestId) {
fetchOptions.headers = {
...fetchOptions.headers,
"x-request-id": ctx.requestId,
};
}
// Add Unix socket path if needed
if (isUnixSocket) {
fetchOptions.unix = upstreamUrl;
@@ -122,7 +122,10 @@
</h2>
<!-- USERNAME ROW: gap-1.5 controls spacing between elements -->
<div class="flex items-center gap-1.5 text-sm">
<span class="font-mono text-xs px-1.5 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 bg-zinc-200/50 dark:bg-zinc-800/50 text-zinc-600 dark:text-zinc-400">{username}</span>
<span
class="font-mono text-xs px-1.5 py-0.5 rounded border border-zinc-300 dark:border-zinc-700 bg-zinc-200/50 dark:bg-zinc-800/50 text-zinc-600 dark:text-zinc-400"
>{username}</span
>
<button
onclick={copyUsername}
class="p-0.5 rounded hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
+3
View File
@@ -1,5 +1,6 @@
import { dev } from "$app/environment";
import { configure, getConsoleSink, type LogRecord } from "@logtape/logtape";
import { requestContext } from "$lib/server/context";
interface RailwayLogEntry {
timestamp: string;
@@ -10,12 +11,14 @@ interface RailwayLogEntry {
}
function railwayFormatter(record: LogRecord): string {
const ctx = requestContext.getStore();
const categoryTarget = record.category.join(":");
const entry: RailwayLogEntry = {
timestamp: new Date().toISOString(),
level: record.level.toLowerCase(),
message: record.message.join(" "),
target: categoryTarget ? `bun:${categoryTarget}` : "bun",
...(ctx?.requestId && { req_id: ctx.requestId }),
};
if (record.properties && Object.keys(record.properties).length > 0) {
+7
View File
@@ -0,0 +1,7 @@
import { AsyncLocalStorage } from "node:async_hooks";
export interface RequestContext {
requestId?: string;
}
export const requestContext = new AsyncLocalStorage<RequestContext>();