UNPKG

next-auto-logger

Version:

Universal Pino-based request logger for Next.js - auto-detects client/server, structured JSON logs for CloudWatch

170 lines (169 loc) 5.55 kB
// src/api.ts import pino from "pino"; var createLogger = () => { const isDev = process.env.NODE_ENV === "development"; return pino({ level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"), formatters: { level: (label) => ({ level: label }), log: (obj) => ({ ...obj, timestamp: (/* @__PURE__ */ new Date()).toISOString(), environment: "server" }) }, // No pino-pretty transport - JSON logs only for reliability ...!isDev && { redact: ["headers.authorization", "headers.cookie", "body.password"] } }); }; var logger = createLogger(); var getClientIP = (req) => { if ("connection" in req) { return req.headers["x-forwarded-for"]?.split(",")[0] || req.headers["x-real-ip"] || req.connection?.remoteAddress || "127.0.0.1"; } else { return req.headers.get("x-forwarded-for")?.split(",")[0] || req.headers.get("x-real-ip") || "127.0.0.1"; } }; var setCorsHeaders = (setHeader, origin) => { setHeader("Access-Control-Allow-Origin", origin || "*"); setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); }; var rateLimitStore = /* @__PURE__ */ new Map(); var checkRateLimit = (ip, limit = 100) => { const now = Date.now(); const windowMs = 6e4; const record = rateLimitStore.get(ip); if (!record || now > record.resetTime) { rateLimitStore.set(ip, { count: 1, resetTime: now + windowMs }); return true; } if (record.count >= limit) return false; record.count++; return true; }; async function handler(req, res) { const origin = req.headers.origin; setCorsHeaders((k, v) => res.setHeader(k, v), origin); if (req.method === "OPTIONS") { return res.status(200).end(); } if (req.method !== "POST") { return res.status(405).json({ error: "Method not allowed" }); } const clientIP = getClientIP(req); if (!checkRateLimit(clientIP)) { return res.status(429).json({ error: "Rate limit exceeded" }); } try { const logData = req.body; if (!logData.event || !logData.requestId || !logData.url) { return res.status(400).json({ error: "Invalid log data structure" }); } const enrichedLog = { ...logData, serverTimestamp: (/* @__PURE__ */ new Date()).toISOString(), clientIP, userAgent: req.headers["user-agent"], referer: req.headers.referer, // Ensure environment is set to client for logs coming from client environment: "client" }; const level = logData.event === "request_error" ? "error" : "info"; logger[level](enrichedLog); return res.status(200).json({ success: true }); } catch (error) { logger.error({ error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0, clientIP: getClientIP(req) }, "Failed to process client log"); return res.status(500).json({ error: "Internal server error" }); } } async function POST(req) { const corsHeaders = {}; const origin = req.headers.get("origin"); if (origin) { corsHeaders["Access-Control-Allow-Origin"] = origin; } else { corsHeaders["Access-Control-Allow-Origin"] = "*"; } corsHeaders["Access-Control-Allow-Methods"] = "POST, OPTIONS"; corsHeaders["Access-Control-Allow-Headers"] = "Content-Type, Authorization"; const clientIP = getClientIP(req); if (!checkRateLimit(clientIP)) { return new Response(JSON.stringify({ error: "Rate limit exceeded" }), { status: 429, headers: { "Content-Type": "application/json", ...corsHeaders } }); } try { const logData = await req.json(); if (!logData.event || !logData.requestId || !logData.url) { return new Response(JSON.stringify({ error: "Invalid log data structure" }), { status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }); } const enrichedLog = { ...logData, serverTimestamp: (/* @__PURE__ */ new Date()).toISOString(), clientIP, userAgent: req.headers.get("user-agent"), referer: req.headers.get("referer"), // Ensure environment is set to client for logs coming from client environment: "client" }; const level = logData.event === "request_error" ? "error" : "info"; logger[level](enrichedLog); return new Response(JSON.stringify({ success: true }), { status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }); } catch (error) { logger.error({ error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0, clientIP: getClientIP(req) }, "Failed to process client log"); return new Response(JSON.stringify({ error: "Internal server error" }), { status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }); } } async function OPTIONS(req) { const corsHeaders = {}; const origin = req.headers.get("origin"); if (origin) { corsHeaders["Access-Control-Allow-Origin"] = origin; } else { corsHeaders["Access-Control-Allow-Origin"] = "*"; } corsHeaders["Access-Control-Allow-Methods"] = "POST, OPTIONS"; corsHeaders["Access-Control-Allow-Headers"] = "Content-Type, Authorization"; return new Response(null, { status: 200, headers: corsHeaders }); } export { OPTIONS, POST, handler as default };