Skip to content

Commit 1e687c7

Browse files
Merge PR #732
2 parents 719e5e6 + d758b43 commit 1e687c7

2 files changed

Lines changed: 94 additions & 6 deletions

File tree

src/middleware/accessLog.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
* The log name is selected by route prefix so consumers can filter access logs
2424
* more easily: `/api/users` => `users_access_log`, `/api/auth` =>
2525
* `auth_access_log`, `/api/predictions` => `predictions_access_log`,
26-
* `/api/markets` => `markets_access_log`, and `/api/tags` =>
27-
* `tags_access_log`.
26+
* `/api/markets` => `markets_access_log`, `/api/tags` =>
27+
* `tags_access_log`, `/api/feature-flags` => `feature_flags_access_log`,
28+
* and `/api/referrals` => `referrals_access_log`.
2829
*
2930
* Usage
3031
* -----
@@ -98,7 +99,7 @@ function resolveIp(req: Request): string {
9899
*
99100
* Stamps `res.locals.correlationId` and hooks `res.on("finish")` to emit
100101
* a `users_access_log`, `auth_access_log`, `predictions_access_log`,
101-
* `markets_access_log`, or `tags_access_log` log entry once the response
102+
* `markets_access_log`, `tags_access_log`, or `referrals_access_log` log entry once the response
102103
* has been flushed.
103104
* Always calls `next()` so it is safe to mount as the first middleware on
104105
* any router without affecting the handler chain.
@@ -129,8 +130,10 @@ export function accessLog(req: Request, res: Response, next: NextFunction): void
129130
logName = "markets_access_log";
130131
} else if (req.originalUrl.startsWith("/api/feature-flags")) {
131132
logName = "feature_flags_access_log";
132-
} else if (req.originalUrl.startsWith("/api/webhooks")) {
133-
logName = "webhooks_access_log";
133+
} else if (req.originalUrl.startsWith("/api/tags")) {
134+
logName = "tags_access_log";
135+
} else if (req.originalUrl.startsWith("/api/referrals")) {
136+
logName = "referrals_access_log";
134137
}
135138

136139
const durationMs = Date.now() - startMs;

tests/usersAccessLog.test.ts

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ describe("accessLog middleware", () => {
254254

255255
// ── Structured log on finish ───────────────────────────────────────────
256256

257-
it("emits a users_access_log entry on response finish with required fields", async () => {
257+
it("emits a users_access_log entry on response finish with all required fields", async () => {
258258
const req = makeReq({
259259
headers: { "x-correlation-id": "log-test-id" },
260260
method: "GET",
@@ -269,12 +269,17 @@ describe("accessLog middleware", () => {
269269

270270
expect(loggerInfoSpy).toHaveBeenCalledWith(
271271
expect.objectContaining({
272+
"req-id": "log-test-id",
272273
correlationId: "log-test-id",
273274
method: "GET",
274275
path: "/api/users/me",
275276
statusCode: 200,
277+
status: 200,
276278
ip: "10.0.0.1",
277279
durationMs: expect.any(Number),
280+
latency: expect.any(Number),
281+
size: 0,
282+
actor: "anonymous",
278283
}),
279284
"users_access_log",
280285
);
@@ -493,4 +498,84 @@ describe("accessLog middleware", () => {
493498
"users_access_log",
494499
);
495500
});
501+
502+
// ── Content-Length / size ──────────────────────────────────────────────
503+
504+
it("logs the Content-Length when set on the response", async () => {
505+
const req = makeReq({ headers: { "x-correlation-id": "size-test-id" } });
506+
const res = makeRes();
507+
res.setHeader("Content-Length", "512");
508+
const next: NextFunction = jest.fn();
509+
510+
accessLog(req, res, next);
511+
await fireFinish(res);
512+
513+
expect(loggerInfoSpy).toHaveBeenCalledWith(
514+
expect.objectContaining({ size: 512 }),
515+
"users_access_log",
516+
);
517+
});
518+
519+
it("logs size=0 when Content-Length is absent", async () => {
520+
const req = makeReq({ headers: { "x-correlation-id": "no-size-id" } });
521+
const res = makeRes();
522+
const next: NextFunction = jest.fn();
523+
524+
accessLog(req, res, next);
525+
await fireFinish(res);
526+
527+
expect(loggerInfoSpy).toHaveBeenCalledWith(
528+
expect.objectContaining({ size: 0 }),
529+
"users_access_log",
530+
);
531+
});
532+
533+
// ── Actor extraction ───────────────────────────────────────────────────
534+
535+
it("logs the authenticated actor when req.user is set", async () => {
536+
const req = makeReq({
537+
headers: { "x-correlation-id": "actor-test-id" },
538+
method: "POST",
539+
path: "/api/users/me",
540+
});
541+
(req as any).user = { id: "authenticated-user-id" };
542+
const res = makeRes();
543+
const next: NextFunction = jest.fn();
544+
545+
accessLog(req, res, next);
546+
await fireFinish(res);
547+
548+
expect(loggerInfoSpy).toHaveBeenCalledWith(
549+
expect.objectContaining({ actor: "authenticated-user-id" }),
550+
"users_access_log",
551+
);
552+
});
553+
554+
// ── referrals_access_log ──────────────────────────────────────────────
555+
556+
it("emits a referrals_access_log entry when originalUrl starts with /api/referrals", async () => {
557+
const req = makeReq({
558+
headers: { "x-correlation-id": "ref-log-test-id" },
559+
method: "POST",
560+
path: "/api/referrals",
561+
ip: "10.0.0.5",
562+
});
563+
const res = makeRes();
564+
const next: NextFunction = jest.fn();
565+
566+
accessLog(req, res, next);
567+
await fireFinish(res);
568+
569+
expect(loggerInfoSpy).toHaveBeenCalledWith(
570+
expect.objectContaining({
571+
correlationId: "ref-log-test-id",
572+
method: "POST",
573+
path: "/api/referrals",
574+
statusCode: 200,
575+
ip: "10.0.0.5",
576+
durationMs: expect.any(Number),
577+
}),
578+
"referrals_access_log",
579+
);
580+
});
496581
});

0 commit comments

Comments
 (0)