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
JavaScript
// 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
};