mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 14:26:37 -06:00
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:
+13
-6
@@ -6,13 +6,20 @@ const API_SOCKET = "/tmp/api.sock";
|
||||
const PORT = process.env.PORT || "8080";
|
||||
const LOG_JSON = process.env.LOG_JSON || "true";
|
||||
|
||||
function tryUnlink(path: string) {
|
||||
try {
|
||||
unlinkSync(path);
|
||||
} catch (e) {
|
||||
// ENOENT is expected (socket doesn't exist yet), other errors are unexpected
|
||||
if (e instanceof Error && "code" in e && e.code !== "ENOENT") {
|
||||
console.error(`Failed to cleanup ${path}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
try {
|
||||
unlinkSync(BUN_SOCKET);
|
||||
} catch {}
|
||||
try {
|
||||
unlinkSync(API_SOCKET);
|
||||
} catch {}
|
||||
tryUnlink(BUN_SOCKET);
|
||||
tryUnlink(API_SOCKET);
|
||||
}
|
||||
|
||||
// Cleanup on signals
|
||||
|
||||
+39
-24
@@ -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 ({
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
|
||||
export interface RequestContext {
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export const requestContext = new AsyncLocalStorage<RequestContext>();
|
||||
@@ -29,6 +29,9 @@ function stripAnsi(str: string): string {
|
||||
return str.replace(/\u001b\[[0-9;]*m/g, "").trim();
|
||||
}
|
||||
|
||||
// Module-level flag to prevent reconfiguration across plugin instantiations
|
||||
let loggerConfigured = false;
|
||||
|
||||
export function jsonLogger(): Plugin {
|
||||
const useJsonLogs =
|
||||
process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1";
|
||||
@@ -39,7 +42,6 @@ export function jsonLogger(): Plugin {
|
||||
};
|
||||
}
|
||||
|
||||
let loggerConfigured = false;
|
||||
const configureLogger = async () => {
|
||||
if (loggerConfigured) return;
|
||||
await configure({
|
||||
|
||||
Reference in New Issue
Block a user