rtrm HF Staff commited on
Commit
b70b749
·
unverified ·
1 Parent(s): c178e57

fix(logger): contextual logs (#2019)

Browse files
src/hooks.server.ts CHANGED
@@ -1,5 +1,11 @@
1
  import { config, ready } from "$lib/server/config";
2
- import type { Handle, HandleServerError, ServerInit, HandleFetch } from "@sveltejs/kit";
 
 
 
 
 
 
3
  import { collections } from "$lib/server/database";
4
  import { base } from "$app/paths";
5
  import {
@@ -20,6 +26,15 @@ import { adminTokenManager } from "$lib/server/adminToken";
20
  import { isHostLocalhost } from "$lib/server/isURLLocal";
21
  import { MetricsServer } from "$lib/server/metrics";
22
  import { loadMcpServersOnStartup } from "$lib/server/mcp/registry";
 
 
 
 
 
 
 
 
 
23
 
24
  export const init: ServerInit = async () => {
25
  // Wait for config to be fully loaded
@@ -100,194 +115,214 @@ export const handleError: HandleServerError = async ({ error, event, status, mes
100
  };
101
 
102
  export const handle: Handle = async ({ event, resolve }) => {
103
- await ready.then(() => {
104
- config.checkForUpdates();
105
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
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
- },
123
- });
124
- }
125
 
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;
 
128
 
129
- if (!ADMIN_SECRET) {
130
- return errorResponse(500, "Admin API is not configured");
131
- }
 
132
 
133
- if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) {
134
- return errorResponse(401, "Unauthorized");
135
- }
136
- }
 
137
 
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);
156
  }
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);
172
- }
173
- }
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);
 
181
 
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");
 
194
 
195
- if (!origin) {
196
- return errorResponse(403, "Non-JSON form requests need to have an origin");
 
 
197
  }
198
 
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");
 
 
206
  }
207
- }
208
- }
209
-
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;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
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;
244
 
245
- return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
246
- },
247
- filterSerializedResponseHeaders: (header) => {
248
- return header.includes("content-type");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  },
250
- });
251
-
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
- }
256
-
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
- }
263
-
264
- if (event.url.pathname.startsWith(`${base}/api/`)) {
265
- // get origin from the request
266
- const requestOrigin = event.request.headers.get("origin");
267
-
268
- // get origin from the config if its defined
269
- let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined;
270
-
271
- 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
275
- ) {
276
- allowedOrigin = "*"; // allow all origins
277
- } else if (allowedOrigin === requestOrigin) {
278
- allowedOrigin = requestOrigin; // echo back the caller
279
- }
280
-
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");
288
- }
289
- }
290
- return response;
291
  };
292
 
293
  export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
 
1
  import { config, ready } from "$lib/server/config";
2
+ import type {
3
+ Handle,
4
+ HandleServerError,
5
+ ServerInit,
6
+ HandleFetch,
7
+ RequestEvent,
8
+ } from "@sveltejs/kit";
9
  import { collections } from "$lib/server/database";
10
  import { base } from "$app/paths";
11
  import {
 
26
  import { isHostLocalhost } from "$lib/server/isURLLocal";
27
  import { MetricsServer } from "$lib/server/metrics";
28
  import { loadMcpServersOnStartup } from "$lib/server/mcp/registry";
29
+ import { runWithRequestContext, updateRequestContext } from "$lib/server/requestContext";
30
+
31
+ function getClientAddressSafe(event: RequestEvent): string | undefined {
32
+ try {
33
+ return event.getClientAddress();
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
38
 
39
  export const init: ServerInit = async () => {
40
  // Wait for config to be fully loaded
 
115
  };
116
 
117
  export const handle: Handle = async ({ event, resolve }) => {
118
+ // Generate a unique request ID for this request
119
+ const requestId = crypto.randomUUID();
120
+
121
+ // Run the entire request handling within the request context
122
+ return runWithRequestContext(
123
+ async () => {
124
+ await ready.then(() => {
125
+ config.checkForUpdates();
126
+ });
127
+
128
+ logger.debug(
129
+ {
130
+ locals: event.locals,
131
+ url: event.url.pathname,
132
+ params: event.params,
133
+ request: event.request,
134
+ },
135
+ "Request received"
136
+ );
137
 
138
+ function errorResponse(status: number, message: string) {
139
+ const sendJson =
140
+ event.request.headers.get("accept")?.includes("application/json") ||
141
+ event.request.headers.get("content-type")?.includes("application/json");
142
+ return new Response(sendJson ? JSON.stringify({ error: message }) : message, {
143
+ status,
144
+ headers: {
145
+ "content-type": sendJson ? "application/json" : "text/plain",
146
+ },
147
+ });
148
+ }
149
 
150
+ if (
151
+ event.url.pathname.startsWith(`${base}/admin/`) ||
152
+ event.url.pathname === `${base}/admin`
153
+ ) {
154
+ const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET;
 
 
 
 
 
 
155
 
156
+ if (!ADMIN_SECRET) {
157
+ return errorResponse(500, "Admin API is not configured");
158
+ }
159
 
160
+ if (event.request.headers.get("Authorization") !== `Bearer ${ADMIN_SECRET}`) {
161
+ return errorResponse(401, "Unauthorized");
162
+ }
163
+ }
164
 
165
+ const auth = await authenticateRequest(
166
+ { type: "svelte", value: event.request.headers },
167
+ { type: "svelte", value: event.cookies },
168
+ event.url
169
+ );
170
 
171
+ event.locals.sessionId = auth.sessionId;
172
+
173
+ if (loginEnabled && !auth.user && !event.url.pathname.startsWith(`${base}/.well-known/`)) {
174
+ if (config.AUTOMATIC_LOGIN === "true") {
175
+ // AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
176
+ if (
177
+ !event.url.pathname.startsWith(`${base}/login`) &&
178
+ !event.url.pathname.startsWith(`${base}/healthcheck`)
179
+ ) {
180
+ // To get the same CSRF token after callback
181
+ refreshSessionCookie(event.cookies, auth.secretSessionId);
182
+ return await triggerOauthFlow(event);
183
+ }
184
+ } else {
185
+ // Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
186
+ if (
187
+ event.url.pathname !== `${base}/` &&
188
+ event.url.pathname !== `${base}` &&
189
+ !event.url.pathname.startsWith(`${base}/login`) &&
190
+ !event.url.pathname.startsWith(`${base}/login/callback`) &&
191
+ !event.url.pathname.startsWith(`${base}/healthcheck`) &&
192
+ !event.url.pathname.startsWith(`${base}/r/`) &&
193
+ !event.url.pathname.startsWith(`${base}/conversation/`) &&
194
+ !event.url.pathname.startsWith(`${base}/models/`) &&
195
+ !event.url.pathname.startsWith(`${base}/api`)
196
+ ) {
197
+ refreshSessionCookie(event.cookies, auth.secretSessionId);
198
+ return triggerOauthFlow(event);
199
+ }
200
+ }
201
+ }
202
 
203
+ event.locals.user = auth.user || undefined;
204
+ event.locals.token = auth.token;
205
 
206
+ // Update request context with user after authentication
207
+ if (auth.user?.username) {
208
+ updateRequestContext({ user: auth.user.username });
 
 
 
 
 
 
 
209
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
+ event.locals.isAdmin =
212
+ event.locals.user?.isAdmin || adminTokenManager.isAdmin(event.locals.sessionId);
213
+
214
+ // CSRF protection
215
+ const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
216
+ /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#attr-enctype */
217
+ const nativeFormContentTypes = [
218
+ "multipart/form-data",
219
+ "application/x-www-form-urlencoded",
220
+ "text/plain",
221
+ ];
222
 
223
+ if (event.request.method === "POST") {
224
+ if (nativeFormContentTypes.includes(requestContentType)) {
225
+ const origin = event.request.headers.get("origin");
226
 
227
+ if (!origin) {
228
+ return errorResponse(403, "Non-JSON form requests need to have an origin");
229
+ }
 
 
 
 
 
230
 
231
+ const validOrigins = [
232
+ new URL(event.request.url).host,
233
+ ...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []),
234
+ ];
235
 
236
+ if (!validOrigins.includes(new URL(origin).host)) {
237
+ return errorResponse(403, "Invalid referer for POST request");
238
+ }
239
+ }
240
  }
241
 
242
+ if (
243
+ event.request.method === "POST" ||
244
+ event.url.pathname.startsWith(`${base}/login`) ||
245
+ event.url.pathname.startsWith(`${base}/login/callback`)
246
+ ) {
247
+ // if the request is a POST request or login-related we refresh the cookie
248
+ refreshSessionCookie(event.cookies, auth.secretSessionId);
249
 
250
+ await collections.sessions.updateOne(
251
+ { sessionId: auth.sessionId },
252
+ { $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
253
+ );
254
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
+ if (
257
+ loginEnabled &&
258
+ !event.locals.user &&
259
+ !event.url.pathname.startsWith(`${base}/login`) &&
260
+ !event.url.pathname.startsWith(`${base}/admin`) &&
261
+ !event.url.pathname.startsWith(`${base}/settings`) &&
262
+ !["GET", "OPTIONS", "HEAD"].includes(event.request.method)
263
+ ) {
264
+ return errorResponse(401, ERROR_MESSAGES.authOnly);
265
+ }
266
 
267
+ let replaced = false;
268
+
269
+ const response = await resolve(event, {
270
+ transformPageChunk: (chunk) => {
271
+ // For some reason, Sveltekit doesn't let us load env variables from .env in the app.html template
272
+ if (replaced || !chunk.html.includes("%gaId%")) {
273
+ return chunk.html;
274
+ }
275
+ replaced = true;
276
+
277
+ return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
278
+ },
279
+ filterSerializedResponseHeaders: (header) => {
280
+ return header.includes("content-type");
281
+ },
282
+ });
283
+
284
+ // Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
285
+ if (config.ALLOW_IFRAME !== "true") {
286
+ response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
287
+ }
288
 
289
+ if (
290
+ event.url.pathname.startsWith(`${base}/login/callback`) ||
291
+ event.url.pathname.startsWith(`${base}/login`)
292
+ ) {
293
+ response.headers.append("Cache-Control", "no-store");
294
  }
 
295
 
296
+ if (event.url.pathname.startsWith(`${base}/api/`)) {
297
+ // get origin from the request
298
+ const requestOrigin = event.request.headers.get("origin");
299
+
300
+ // get origin from the config if its defined
301
+ let allowedOrigin = config.PUBLIC_ORIGIN ? new URL(config.PUBLIC_ORIGIN).origin : undefined;
302
+
303
+ if (
304
+ dev || // if we're in dev mode
305
+ !requestOrigin || // or the origin is null (SSR)
306
+ isHostLocalhost(new URL(requestOrigin).hostname) // or the origin is localhost
307
+ ) {
308
+ allowedOrigin = "*"; // allow all origins
309
+ } else if (allowedOrigin === requestOrigin) {
310
+ allowedOrigin = requestOrigin; // echo back the caller
311
+ }
312
+
313
+ if (allowedOrigin) {
314
+ response.headers.set("Access-Control-Allow-Origin", allowedOrigin);
315
+ response.headers.set(
316
+ "Access-Control-Allow-Methods",
317
+ "GET, POST, PUT, PATCH, DELETE, OPTIONS"
318
+ );
319
+ response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
320
+ }
321
+ }
322
+ return response;
323
  },
324
+ { requestId, url: event.url.pathname, ip: getClientAddressSafe(event) }
325
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  };
327
 
328
  export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
src/lib/server/logger.ts CHANGED
@@ -1,6 +1,7 @@
1
  import pino from "pino";
2
  import { dev } from "$app/environment";
3
  import { config } from "$lib/server/config";
 
4
 
5
  let options: pino.LoggerOptions = {};
6
 
@@ -15,7 +16,7 @@ if (dev) {
15
  };
16
  }
17
 
18
- export const logger = pino({
19
  ...options,
20
  messageKey: "message",
21
  level: config.LOG_LEVEL || "info",
@@ -24,4 +25,17 @@ export const logger = pino({
24
  return { level: label };
25
  },
26
  },
 
 
 
 
 
 
 
 
 
 
 
27
  });
 
 
 
1
  import pino from "pino";
2
  import { dev } from "$app/environment";
3
  import { config } from "$lib/server/config";
4
+ import { getRequestContext } from "$lib/server/requestContext";
5
 
6
  let options: pino.LoggerOptions = {};
7
 
 
16
  };
17
  }
18
 
19
+ const baseLogger = pino({
20
  ...options,
21
  messageKey: "message",
22
  level: config.LOG_LEVEL || "info",
 
25
  return { level: label };
26
  },
27
  },
28
+ mixin() {
29
+ const ctx = getRequestContext();
30
+ if (!ctx) return {};
31
+
32
+ const result: Record<string, string> = {};
33
+ if (ctx.requestId) result.requestId = ctx.requestId;
34
+ if (ctx.url) result.url = ctx.url;
35
+ if (ctx.ip) result.ip = ctx.ip;
36
+ if (ctx.user) result.user = ctx.user;
37
+ return result;
38
+ },
39
  });
40
+
41
+ export const logger = baseLogger;
src/lib/server/requestContext.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ export interface RequestContext {
5
+ requestId: string;
6
+ url?: string;
7
+ ip?: string;
8
+ user?: string;
9
+ }
10
+
11
+ const asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
12
+
13
+ /**
14
+ * Run a function within a request context.
15
+ * All logs within this context will automatically include the requestId.
16
+ */
17
+ export function runWithRequestContext<T>(
18
+ fn: () => T,
19
+ context: Partial<RequestContext> & { requestId?: string } = {}
20
+ ): T {
21
+ const fullContext: RequestContext = {
22
+ requestId: context.requestId ?? randomUUID(),
23
+ url: context.url,
24
+ ip: context.ip,
25
+ user: context.user,
26
+ };
27
+ return asyncLocalStorage.run(fullContext, fn);
28
+ }
29
+
30
+ /**
31
+ * Update the current request context with additional information.
32
+ * Useful for adding user information after authentication.
33
+ */
34
+ export function updateRequestContext(updates: Partial<Omit<RequestContext, "requestId">>): void {
35
+ const store = asyncLocalStorage.getStore();
36
+ if (store) {
37
+ Object.assign(store, updates);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get the current request context, if any.
43
+ */
44
+ export function getRequestContext(): RequestContext | undefined {
45
+ return asyncLocalStorage.getStore();
46
+ }
47
+
48
+ /**
49
+ * Get the current request ID, or undefined if not in a request context.
50
+ */
51
+ export function getRequestId(): string | undefined {
52
+ return asyncLocalStorage.getStore()?.requestId;
53
+ }