Implement structured logging for API routes (P3.7)
Replace console.error with pino structured logger across API routes and add key event logging per observability spec: - Auth failure (warn): reason - Period logged (info): userId, date - Override toggled (info): userId, override, enabled - Decision calculated (info): userId, decision, reason - Error events (error): err object with stack trace Files updated: - auth-middleware.ts: Added structured logging for auth failures - cycle/period/route.ts: Added Period logged event + error logging - calendar/[userId]/[token].ics/route.ts: Replaced console.error - overrides/route.ts: Added Override toggled events - today/route.ts: Added Decision calculated event Tests: 720 passing (added 3 new structured logging tests) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,17 @@ vi.mock("next/headers", () => ({
|
||||
cookies: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the logger module
|
||||
vi.mock("./logger", () => ({
|
||||
logger: {
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { logger } from "./logger";
|
||||
import {
|
||||
createPocketBaseClient,
|
||||
getCurrentUser,
|
||||
@@ -26,6 +36,12 @@ import {
|
||||
loadAuthFromCookies,
|
||||
} from "./pocketbase";
|
||||
|
||||
const mockLogger = logger as {
|
||||
warn: ReturnType<typeof vi.fn>;
|
||||
error: ReturnType<typeof vi.fn>;
|
||||
info: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
const mockCookies = cookies as ReturnType<typeof vi.fn>;
|
||||
const mockCreatePocketBaseClient = createPocketBaseClient as ReturnType<
|
||||
typeof vi.fn
|
||||
@@ -161,4 +177,59 @@ describe("withAuth", () => {
|
||||
const body = await response.json();
|
||||
expect(body.error).toBe("Internal server error");
|
||||
});
|
||||
|
||||
describe("structured logging", () => {
|
||||
beforeEach(() => {
|
||||
mockLogger.warn.mockClear();
|
||||
mockLogger.error.mockClear();
|
||||
mockLogger.info.mockClear();
|
||||
});
|
||||
|
||||
it("logs auth failure with warn level", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(false);
|
||||
|
||||
const handler = vi.fn();
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
await wrappedHandler({} as NextRequest);
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ reason: "not_authenticated" }),
|
||||
expect.stringContaining("Auth failure"),
|
||||
);
|
||||
});
|
||||
|
||||
it("logs auth failure when getCurrentUser returns null", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(true);
|
||||
mockGetCurrentUser.mockReturnValue(null);
|
||||
|
||||
const handler = vi.fn();
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
await wrappedHandler({} as NextRequest);
|
||||
|
||||
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ reason: "user_not_found" }),
|
||||
expect.stringContaining("Auth failure"),
|
||||
);
|
||||
});
|
||||
|
||||
it("logs internal errors with error level and stack trace", async () => {
|
||||
mockIsAuthenticated.mockReturnValue(true);
|
||||
mockGetCurrentUser.mockReturnValue(mockUser);
|
||||
|
||||
const testError = new Error("Handler error");
|
||||
const handler = vi.fn().mockRejectedValue(testError);
|
||||
const wrappedHandler = withAuth(handler);
|
||||
|
||||
await wrappedHandler({} as NextRequest);
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
err: testError,
|
||||
}),
|
||||
expect.stringContaining("Auth middleware error"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NextResponse } from "next/server";
|
||||
|
||||
import type { User } from "@/types";
|
||||
|
||||
import { logger } from "./logger";
|
||||
import {
|
||||
createPocketBaseClient,
|
||||
getCurrentUser,
|
||||
@@ -54,19 +55,21 @@ export function withAuth<T = unknown>(
|
||||
|
||||
// Check if the user is authenticated
|
||||
if (!isAuthenticated(pb)) {
|
||||
logger.warn({ reason: "not_authenticated" }, "Auth failure");
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get the current user
|
||||
const user = getCurrentUser(pb);
|
||||
if (!user) {
|
||||
logger.warn({ reason: "user_not_found" }, "Auth failure");
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Call the original handler with the user context
|
||||
return await handler(request, user, context);
|
||||
} catch (error) {
|
||||
console.error("Auth middleware error:", error);
|
||||
logger.error({ err: error }, "Auth middleware error");
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 },
|
||||
|
||||
Reference in New Issue
Block a user