Skip to content

Commit 12facbe

Browse files
committed
fix(logger): contextual logs
1 parent 07223ea commit 12facbe

File tree

3 files changed

+207
-160
lines changed

3 files changed

+207
-160
lines changed

src/hooks.server.ts

Lines changed: 166 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { adminTokenManager } from "$lib/server/adminToken";
2020
import { isHostLocalhost } from "$lib/server/isURLLocal";
2121
import { MetricsServer } from "$lib/server/metrics";
2222
import { loadMcpServersOnStartup } from "$lib/server/mcp/registry";
23+
import { runWithRequestContext } from "$lib/server/requestContext";
2324

2425
export const init: ServerInit = async () => {
2526
// Wait for config to be fully loaded
@@ -100,194 +101,200 @@ export const handleError: HandleServerError = async ({ error, event, status, mes
100101
};
101102

102103
export const handle: Handle = async ({ event, resolve }) => {
103-
await ready.then(() => {
104-
config.checkForUpdates();
105-
});
104+
// Generate a unique request ID for this request
105+
const requestId = crypto.randomUUID();
106106

107-
logger.debug({
108-
locals: event.locals,
109-
url: event.url.pathname,
110-
params: event.params,
111-
request: event.request,
112-
});
113-
114-
function errorResponse(status: number, message: string) {
115-
const sendJson =
116-
event.request.headers.get("accept")?.includes("application/json") ||
117-
event.request.headers.get("content-type")?.includes("application/json");
118-
return new Response(sendJson ? JSON.stringify({ error: message }) : message, {
119-
status,
120-
headers: {
121-
"content-type": sendJson ? "application/json" : "text/plain",
122-
},
107+
// Run the entire request handling within the request context
108+
return runWithRequestContext(async () => {
109+
await ready.then(() => {
110+
config.checkForUpdates();
123111
});
124-
}
125112

126-
if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) {
127-
const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET;
113+
logger.debug({
114+
locals: event.locals,
115+
url: event.url.pathname,
116+
params: event.params,
117+
request: event.request,
118+
});
128119

129-
if (!ADMIN_SECRET) {
130-
return errorResponse(500, "Admin API is not configured");
120+
function errorResponse(status: number, message: string) {
121+
const sendJson =
122+
event.request.headers.get("accept")?.includes("application/json") ||
123+
event.request.headers.get("content-type")?.includes("application/json");
124+
return new Response(sendJson ? JSON.stringify({ error: message }) : message, {
125+
status,
126+
headers: {
127+
"content-type": sendJson ? "application/json" : "text/plain",
128+
},
129+
});
131130
}
132131

133-
if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) {
134-
return errorResponse(401, "Unauthorized");
135-
}
136-
}
132+
if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) {
133+
const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET;
137134

138-
const auth = await authenticateRequest(
139-
{ type: "svelte", value: event.request.headers },
140-
{ type: "svelte", value: event.cookies },
141-
event.url
142-
);
143-
144-
event.locals.sessionId = auth.sessionId;
145-
146-
if (loginEnabled && !auth.user && !event.url.pathname.startsWith(`${base}/.well-known/`)) {
147-
if (config.AUTOMATIC_LOGIN === "true") {
148-
// AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
149-
if (
150-
!event.url.pathname.startsWith(`${base}/login`) &&
151-
!event.url.pathname.startsWith(`${base}/healthcheck`)
152-
) {
153-
// To get the same CSRF token after callback
154-
refreshSessionCookie(event.cookies, auth.secretSessionId);
155-
return await triggerOauthFlow(event);
135+
if (!ADMIN_SECRET) {
136+
return errorResponse(500, "Admin API is not configured");
156137
}
157-
} else {
158-
// Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
159-
if (
160-
event.url.pathname !== `${base}/` &&
161-
event.url.pathname !== `${base}` &&
162-
!event.url.pathname.startsWith(`${base}/login`) &&
163-
!event.url.pathname.startsWith(`${base}/login/callback`) &&
164-
!event.url.pathname.startsWith(`${base}/healthcheck`) &&
165-
!event.url.pathname.startsWith(`${base}/r/`) &&
166-
!event.url.pathname.startsWith(`${base}/conversation/`) &&
167-
!event.url.pathname.startsWith(`${base}/models/`) &&
168-
!event.url.pathname.startsWith(`${base}/api`)
169-
) {
170-
refreshSessionCookie(event.cookies, auth.secretSessionId);
171-
return triggerOauthFlow(event);
138+
139+
if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) {
140+
return errorResponse(401, "Unauthorized");
172141
}
173142
}
174-
}
175-
176-
event.locals.user = auth.user || undefined;
177-
event.locals.token = auth.token;
178-
179-
event.locals.isAdmin =
180-
event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
181143

182-
// CSRF protection
183-
const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
184-
/** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
185-
const nativeFormContentTypes = [
186-
"multipart/form-data",
187-
"application/x-www-form-urlencoded",
188-
"text/plain",
189-
];
190-
191-
if (event.request.method === "POST") {
192-
if (nativeFormContentTypes.includes(requestContentType)) {
193-
const origin = event.request.headers.get("origin");
144+
const auth = await authenticateRequest(
145+
{ type: "svelte", value: event.request.headers },
146+
{ type: "svelte", value: event.cookies },
147+
event.url
148+
);
194149

195-
if (!origin) {
196-
return errorResponse(403, "Non-JSON form requests need to have an origin");
150+
event.locals.sessionId = auth.sessionId;
151+
152+
if (loginEnabled && !auth.user && !event.url.pathname.startsWith(`${base}/.well-known/`)) {
153+
if (config.AUTOMATIC_LOGIN === "true") {
154+
// AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
155+
if (
156+
!event.url.pathname.startsWith(`${base}/login`) &&
157+
!event.url.pathname.startsWith(`${base}/healthcheck`)
158+
) {
159+
// To get the same CSRF token after callback
160+
refreshSessionCookie(event.cookies, auth.secretSessionId);
161+
return await triggerOauthFlow(event);
162+
}
163+
} else {
164+
// Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
165+
if (
166+
event.url.pathname !== `${base}/` &&
167+
event.url.pathname !== `${base}` &&
168+
!event.url.pathname.startsWith(`${base}/login`) &&
169+
!event.url.pathname.startsWith(`${base}/login/callback`) &&
170+
!event.url.pathname.startsWith(`${base}/healthcheck`) &&
171+
!event.url.pathname.startsWith(`${base}/r/`) &&
172+
!event.url.pathname.startsWith(`${base}/conversation/`) &&
173+
!event.url.pathname.startsWith(`${base}/models/`) &&
174+
!event.url.pathname.startsWith(`${base}/api`)
175+
) {
176+
refreshSessionCookie(event.cookies, auth.secretSessionId);
177+
return triggerOauthFlow(event);
178+
}
197179
}
180+
}
198181

199-
const validOrigins = [
200-
new URL(event.request.url).host,
201-
...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []),
202-
];
203-
204-
if (!validOrigins.includes(new URL(origin).host)) {
205-
return errorResponse(403, "Invalid referer for POST request");
182+
event.locals.user = auth.user || undefined;
183+
event.locals.token = auth.token;
184+
185+
event.locals.isAdmin =
186+
event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
187+
188+
// CSRF protection
189+
const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
190+
/** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
191+
const nativeFormContentTypes = [
192+
"multipart/form-data",
193+
"application/x-www-form-urlencoded",
194+
"text/plain",
195+
];
196+
197+
if (event.request.method === "POST") {
198+
if (nativeFormContentTypes.includes(requestContentType)) {
199+
const origin = event.request.headers.get("origin");
200+
201+
if (!origin) {
202+
return errorResponse(403, "Non-JSON form requests need to have an origin");
203+
}
204+
205+
const validOrigins = [
206+
new URL(event.request.url).host,
207+
...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []),
208+
];
209+
210+
if (!validOrigins.includes(new URL(origin).host)) {
211+
return errorResponse(403, "Invalid referer for POST request");
212+
}
206213
}
207214
}
208-
}
209215

210-
if (
211-
event.request.method === "POST" ||
212-
event.url.pathname.startsWith(`${base}/login`) ||
213-
event.url.pathname.startsWith(`${base}/login/callback`)
214-
) {
215-
// if the request is a POST request or login-related we refresh the cookie
216-
refreshSessionCookie(event.cookies, auth.secretSessionId);
217-
218-
await collections.sessions.updateOne(
219-
{ sessionId: auth.sessionId },
220-
{ $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
221-
);
222-
}
223-
224-
if (
225-
loginEnabled &&
226-
!event.locals.user &&
227-
!event.url.pathname.startsWith(`${base}/login`) &&
228-
!event.url.pathname.startsWith(`${base}/admin`) &&
229-
!event.url.pathname.startsWith(`${base}/settings`) &&
230-
!["GET", "OPTIONS", "HEAD"].includes(event.request.method)
231-
) {
232-
return errorResponse(401, ERROR_MESSAGES.authOnly);
233-
}
234-
235-
let replaced = false;
216+
if (
217+
event.request.method === "POST" ||
218+
event.url.pathname.startsWith(`${base}/login`) ||
219+
event.url.pathname.startsWith(`${base}/login/callback`)
220+
) {
221+
// if the request is a POST request or login-related we refresh the cookie
222+
refreshSessionCookie(event.cookies, auth.secretSessionId);
236223

237-
const response = await resolve(event, {
238-
transformPageChunk: (chunk) => {
239-
// For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
240-
if (replaced || !chunk.html.includes("%gaId%")) {
241-
return chunk.html;
242-
}
243-
replaced = true;
224+
await collections.sessions.updateOne(
225+
{ sessionId: auth.sessionId },
226+
{ $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
227+
);
228+
}
244229

245-
return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
246-
},
247-
filterSerializedResponseHeaders: (header) => {
248-
return header.includes("content-type");
249-
},
250-
});
230+
if (
231+
loginEnabled &&
232+
!event.locals.user &&
233+
!event.url.pathname.startsWith(`${base}/login`) &&
234+
!event.url.pathname.startsWith(`${base}/admin`) &&
235+
!event.url.pathname.startsWith(`${base}/settings`) &&
236+
!["GET", "OPTIONS", "HEAD"].includes(event.request.method)
237+
) {
238+
return errorResponse(401, ERROR_MESSAGES.authOnly);
239+
}
251240

252-
// Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
253-
if (config.ALLOW_IFRAME !== "true") {
254-
response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
255-
}
241+
let replaced = false;
256242

257-
if (
258-
event.url.pathname.startsWith(`${base}/login/callback`) ||
259-
event.url.pathname.startsWith(`${base}/login`)
260-
) {
261-
response.headers.append("Cache-Control", "no-store");
262-
}
243+
const response = await resolve(event, {
244+
transformPageChunk: (chunk) => {
245+
// For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
246+
if (replaced || !chunk.html.includes("%gaId%")) {
247+
return chunk.html;
248+
}
249+
replaced = true;
263250

264-
if (event.url.pathname.startsWith(`${base}/api/`)) {
265-
// get origin from the request
266-
const requestOrigin = event.request.headers.get("origin");
251+
return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
252+
},
253+
filterSerializedResponseHeaders: (header) => {
254+
return header.includes("content-type");
255+
},
256+
});
267257

268-
// get origin from the config if its defined
269-
let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined;
258+
// Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
259+
if (config.ALLOW_IFRAME !== "true") {
260+
response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
261+
}
270262

271263
if (
272-
dev || // if we're in dev mode
273-
!requestOrigin || // or the origin is null (SSR)
274-
isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost
264+
event.url.pathname.startsWith(`${base}/login/callback`) ||
265+
event.url.pathname.startsWith(`${base}/login`)
275266
) {
276-
allowedOrigin = "*"; // allow all origins
277-
} else if (allowedOrigin === requestOrigin) {
278-
allowedOrigin = requestOrigin; // echo back the caller
267+
response.headers.append("Cache-Control", "no-store");
279268
}
280269

281-
if (allowedOrigin) {
282-
response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
283-
response.headers.set(
284-
"Access-Control-Allow-Methods",
285-
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
286-
);
287-
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
270+
if (event.url.pathname.startsWith(`${base}/api/`)) {
271+
// get origin from the request
272+
const requestOrigin = event.request.headers.get("origin");
273+
274+
// get origin from the config if its defined
275+
let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined;
276+
277+
if (
278+
dev || // if we're in dev mode
279+
!requestOrigin || // or the origin is null (SSR)
280+
isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost
281+
) {
282+
allowedOrigin = "*"; // allow all origins
283+
} else if (allowedOrigin === requestOrigin) {
284+
allowedOrigin = requestOrigin; // echo back the caller
285+
}
286+
287+
if (allowedOrigin) {
288+
response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
289+
response.headers.set(
290+
"Access-Control-Allow-Methods",
291+
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
292+
);
293+
response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
294+
}
288295
}
289-
}
290-
return response;
296+
return response;
297+
}, requestId);
291298
};
292299

293300
export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {

src/lib/server/logger.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pino from "pino";
22
import { dev } from "$app/environment";
33
import { config } from "$lib/server/config";
4+
import { getRequestId } from "$lib/server/requestContext";
45

56
let options: pino.LoggerOptions = {};
67

@@ -15,7 +16,7 @@ if (dev) {
1516
};
1617
}
1718

18-
export const logger = pino({
19+
const baseLogger = pino({
1920
...options,
2021
messageKey: "message",
2122
level: config.LOG_LEVEL || "info",
@@ -24,4 +25,10 @@ export const logger = pino({
2425
return { level: label };
2526
},
2627
},
28+
mixin() {
29+
const requestId = getRequestId();
30+
return requestId ? { requestId } : {};
31+
},
2732
});
33+
34+
export const logger = baseLogger;

0 commit comments

Comments
 (0)