swadhinbiswas

[email protected]
2026-08-15 09:40:31 +0600 committed NaN years ago
962ac21
commit 962ac21
6 files changed
+153 -14
▼astro.config.mjs
+1-0
35
35
},
36
36
ssr: {
37
37
noExternal: ["@radix-ui/*"],
38
+ external: ["node:async_hooks"],
38
39
},
39
40
},
40
41
});
41
42
▼src/lib/logger.ts
+22-3
1
1
import pino from "pino";
2
+import { getRequestContext } from "./request-context";
2
3
3
4
// Custom OTLP transport for Grafana Cloud
4
5
const createOtlpTransport = () => {
85
86
// Wrap logger to also send to OTLP if configured
86
87
const createWrappedLogger = () => {
87
88
const wrap = (level: string) => (msg: string | object, ...args: unknown[]) => {
89
+ // Bind the current request context (requestId/userId) onto the line
90
+ const ctx = getRequestContext();
91
+ const bound = ctx
92
+ ? baseLogger.child({ requestId: ctx.requestId, userId: ctx.userId })
93
+ : baseLogger;
88
94
// Call original pino method
89
- (baseLogger as unknown as Record<string, Function>)[level](msg, ...args);
95
+ (bound as unknown as Record<string, Function>)[level](msg, ...args);
90
96
91
97
// Send to OTLP if configured
92
98
if (otlpTransport) {
93
99
const message = typeof msg === 'string' ? msg : JSON.stringify(msg);
94
100
const extra = typeof msg === 'object' ? msg : (args[0] as Record<string, unknown>) || {};
95
- otlpTransport.send(level, message, extra as Record<string, unknown>);
101
+ otlpTransport.send(level, message, { ...(ctx || {}), ...extra } as Record<string, unknown>);
96
102
}
97
103
};
98
104
108
114
};
109
115
};
110
116
111
-export const logger = otlpTransport ? createWrappedLogger() : baseLogger;
117
+export const logger = otlpTransport ? createWrappedLogger() : (() => {
118
+ // Proxy binds the request context onto every log call
119
+ const bind = (target: typeof baseLogger) => {
120
+ const ctx = getRequestContext();
121
+ return ctx ? target.child({ requestId: ctx.requestId, userId: ctx.userId }) : target;
122
+ };
123
+ return new Proxy(baseLogger, {
124
+ get(target, prop, receiver) {
125
+ const bound = bind(target);
126
+ const value = Reflect.get(bound, prop, receiver);
127
+ return typeof value === "function" ? value.bind(bound) : value;
128
+ },
129
+ });
130
+})();
112
131
▼src/lib/request-context.ts
+56-0
1
+/**
2
+ * Per-request context (correlation IDs).
3
+ *
4
+ * Every request runs inside an AsyncLocalStorage scope carrying a
5
+ * requestId (+ userId when authenticated). The logger binds these onto
6
+ * every log line, so a single request's logs can be traced end-to-end
7
+ * through DB, git, and CI operations.
8
+ *
9
+ * `node:async_hooks` is initialized ONCE at server boot (middleware calls
10
+ * ensureRequestStorage); after that all reads are synchronous. The dynamic
11
+ * import keeps the Vite browser build from choking on the node builtin.
12
+ */
13
+import type { AsyncLocalStorage as AsyncLocalStorageType } from "node:async_hooks";
14
+
15
+export interface RequestContext {
16
+ requestId: string;
17
+ userId?: string;
18
+ route?: string;
19
+}
20
+
21
+let storageImpl: AsyncLocalStorageType<RequestContext> | null = null;
22
+let storagePromise: Promise<void> | null = null;
23
+
24
+/** Initialize the ALS storage once (server boot). Safe to call repeatedly. */
25
+export function ensureRequestStorage(): Promise<void> {
26
+ if (storageImpl) return Promise.resolve();
27
+ if (!storagePromise) {
28
+ storagePromise = import("node:async_hooks")
29
+ .then((mod) => {
30
+ storageImpl = new mod.AsyncLocalStorage<RequestContext>();
31
+ })
32
+ .catch((err) => {
33
+ storagePromise = null;
34
+ // In browser bundles this import never resolves — fail soft
35
+ console.error("[request-context] async_hooks init failed", err);
36
+ });
37
+ }
38
+ return storagePromise;
39
+}
40
+
41
+/** Sync accessor — valid only after ensureRequestStorage resolved. */
42
+export function getRequestContext(): RequestContext | undefined {
43
+ return storageImpl?.getStore();
44
+}
45
+
46
+export function withRequestContext<T>(
47
+ ctx: RequestContext,
48
+ fn: () => T,
49
+): T {
50
+ if (!storageImpl) return fn(); // not initialized (browser/test) — passthrough
51
+ return storageImpl.run(ctx, fn);
52
+}
53
+
54
+export function newRequestId(): string {
55
+ return `req_${crypto.randomUUID().slice(0, 18)}`;
56
+}
0
57
▼src/middleware.ts
+27-1
1
1
import { defineMiddleware } from "astro:middleware";
2
+import type { MiddlewareHandler } from "astro";
2
3
import { eq } from "drizzle-orm";
3
4
import { getDatabase, schema } from "./db";
4
5
import { getUserFromRequest } from "./lib/auth";
7
8
import { applyCsrfProtection } from "./middleware/csrf";
8
9
import { createRateLimitMiddleware } from "./middleware/rate-limit";
9
10
import { isConfigured } from "./lib/config";
11
+import {
12
+ ensureRequestStorage,
13
+ newRequestId,
14
+ withRequestContext,
15
+} from "./lib/request-context";
10
16
11
17
// Define tiers for different routes
12
18
const apiLimiter = createRateLimitMiddleware("api");
24
30
export const onRequest = defineMiddleware(async (context, next) => {
25
31
const { request, url } = context;
26
32
33
+ // ── Correlation ID: one requestId for every log line in this request ──
34
+ await ensureRequestStorage();
35
+ const requestId =
36
+ request.headers.get("x-request-id") || newRequestId();
37
+ return withRequestContext(
38
+ {
39
+ requestId,
40
+ userId: (context.locals as any)?.user?.id,
41
+ route: url.pathname,
42
+ },
43
+ () => onRequestInner(context, next),
44
+ );
45
+});
46
+
47
+async function onRequestInner(
48
+ context: Parameters<MiddlewareHandler>[0],
49
+ next: () => Promise<Response>,
50
+) {
51
+ const { request, url } = context;
52
+
27
53
// ── Configuration Check ──
28
54
const configured = isConfigured();
29
55
const isSetupRoute = url.pathname === "/setup" || url.pathname.startsWith("/api/setup");
140
166
}
141
167
142
168
return response;
143
-});
169
+}
144
170
▼src/pages/api/repos/index.ts
+18-2
5
5
import { type APIRoute } from 'astro';
6
6
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
7
7
import { z } from 'zod';
8
-import { eq, desc, and, or, like, count } from 'drizzle-orm';
8
+import { eq, desc, and, or, like, count, inArray } from 'drizzle-orm';
9
9
import { getDatabase, schema } from "@/db";
10
10
import { repositories, users } from '@/db/schema';
11
11
import { getUserFromRequest } from '@/lib/auth';
142
142
},
143
143
});
144
144
145
+ // Org-owned repos have a null `owner` relation (ownerId = org id) —
146
+ // resolve org names in one batched query
147
+ const orgIds = repos
148
+ .filter((r) => r.ownerType === "organization")
149
+ .map((r) => r.ownerId);
150
+ let orgNameById = new Map<string, string>();
151
+ if (orgIds.length > 0) {
152
+ const orgs = await db.query.organizations.findMany({
153
+ where: inArray(schema.organizations.id, orgIds),
154
+ columns: { id: true, name: true },
155
+ });
156
+ orgNameById = new Map(orgs.map((o) => [o.id, o.name]));
157
+ }
158
+
145
159
// Transform for response
146
160
const data = repos.map((repo) => ({
147
161
id: repo.id,
148
162
name: repo.name,
149
163
slug: repo.slug,
150
- fullName: `${repo.owner.username}/${repo.name}`,
164
+ fullName: `${
165
+ repo.owner?.username ?? orgNameById.get(repo.ownerId) ?? "unknown"
166
+ }/${repo.name}`,
151
167
description: repo.description,
152
168
visibility: repo.visibility,
153
169
defaultBranch: repo.defaultBranch,
154
170
▼src/pages/explore.astro
+29-8
27
27
where: eq(schema.repositories.visibility, "public"),
28
28
orderBy: [desc(schema.repositories.updatedAt)],
29
29
with: {
30
- owner: true,
30
+ owner: {
31
+ columns: { id: true, username: true, avatarUrl: true, displayName: true },
32
+ },
31
33
},
32
34
limit: perPage,
33
35
offset: offset,
34
36
});
35
37
38
+// Org-owned repos have a null `owner` relation — resolve org names in one pass
39
+const orgIds = repos
40
+ .filter((r) => (r as any).ownerType === "organization")
41
+ .map((r) => r.ownerId);
42
+const orgNameById = new Map<string, string>();
43
+if (orgIds.length > 0) {
44
+ const orgs = await db.query.organizations.findMany({
45
+ where: (t, { inArray }) => inArray(t.id, orgIds),
46
+ columns: { id: true, name: true },
47
+ });
48
+ for (const o of orgs) orgNameById.set(o.id, o.name);
49
+}
50
+
36
51
function timeAgo(date: string | Date) {
37
52
const seconds = Math.floor(
38
53
(new Date().getTime() - new Date(date).getTime()) / 1000,
201
216
</div>
202
217
) : (
203
218
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
204
- {repos.map((repo) => (
219
+ {repos.map((repo) => {
220
+ const ownerName =
221
+ (repo as any).owner?.username ??
222
+ orgNameById.get((repo as any).ownerId) ??
223
+ "unknown";
224
+ return (
205
225
<a
206
- href={`/${repo.owner.username}/${repo.name}`}
226
+ href={`/${ownerName}/${repo.name}`}
207
227
class="group relative"
208
228
>
209
229
<div class="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/20 via-transparent to-purple-500/20 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity" />
210
230
<div class="relative h-full rounded-xl border border-border bg-card/80 backdrop-blur-sm p-5 hover:bg-accent/50 transition-colors flex flex-col">
211
231
<div class="flex items-start gap-3 mb-3">
212
- {repo.owner.avatarUrl ? (
232
+ {(repo as any).owner?.avatarUrl ? (
213
233
<img
214
- src={repo.owner.avatarUrl}
234
+ src={(repo as any).owner.avatarUrl}
215
235
alt=""
216
236
class="h-8 w-8 rounded-full ring-2 ring-border"
217
237
/>
218
238
) : (
219
239
<div class="h-8 w-8 rounded-full bg-gradient-to-br from-cyan-400 to-blue-600 flex items-center justify-center text-sm font-bold text-foreground ring-2 ring-border">
220
- {repo.owner.username[0].toUpperCase()}
240
+ {ownerName[0].toUpperCase()}
221
241
</div>
222
242
)}
223
243
<div class="flex-1 min-w-0">
224
244
<div class="flex items-center gap-2">
225
245
<span class="font-semibold text-foreground group-hover:text-cyan-400 transition-colors truncate">
226
- {repo.owner.username}/{repo.name}
246
+ {ownerName}/{repo.name}
227
247
</span>
228
248
</div>
229
249
<span class="text-xs text-muted-foreground">
274
294
</div>
275
295
</div>
276
296
</a>
277
- ))}
297
+ );
298
+ })}
278
299
</div>
279
300
)
280
301
}
281
302