const logger = require("koa-logger"); const responseTime = require("koa-response-time"); const bodyParser = require("koa-bodyparser"); const Router = require("koa-router"); const Koa = require("koa"); const os = require("os"); const fs = require("fs"); const https = require("https"); const path = require("path"); const app = new Koa(); // Daftar assets yang perlu didownload const requiredAssets = [ { url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_02.png", dest: "assets/pattern_02.png", }, { url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_ny.png", dest: "assets/pattern_ny.png", }, { url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/pattern_ny_old.png", dest: "assets/pattern_ny_old.png", }, { url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-apple-image.json", dest: "assets/emoji/emoji-apple-image.json", }, { url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-google-image.json", dest: "assets/emoji/emoji-google-image.json", }, { url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-joypixels-image.json", dest: "assets/emoji/emoji-joypixels-image.json", }, { url: "https://raw.githubusercontent.com/LyoSU/quote-api/master/assets/emoji/emoji-twitter-image.json", dest: "assets/emoji/emoji-twitter-image.json", }, ]; // Fungsi untuk download file function downloadFile(url, dest) { return new Promise((resolve, reject) => { const dir = path.dirname(dest); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } if (fs.existsSync(dest)) { console.log(`✓ ${dest} already exists`); return resolve(); } const file = fs.createWriteStream(dest); console.log(`⏬ Downloading ${path.basename(dest)}...`); https .get(url, (response) => { if (response.statusCode === 301 || response.statusCode === 302) { return downloadFile(response.headers.location, dest) .then(resolve) .catch(reject); } if (response.statusCode !== 200) { reject( new Error(`Failed to download ${url}: ${response.statusCode}`) ); return; } response.pipe(file); file.on("finish", () => { file.close(); console.log(`✅ Downloaded ${dest}`); resolve(); }); }) .on("error", (err) => { fs.unlink(dest, () => {}); reject(err); }); file.on("error", (err) => { fs.unlink(dest, () => {}); reject(err); }); }); } // Download semua assets yang diperlukan async function ensureAssets() { console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); console.log("🔍 Checking required assets..."); console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); try { for (const asset of requiredAssets) { await downloadFile(asset.url, asset.dest); } console.log("\n✨ All assets ready!\n"); } catch (err) { console.error("\n❌ Error downloading assets:"); console.error(err.message); throw err; } } // Custom logger untuk melihat request body dan error detail app.use(async (ctx, next) => { const start = Date.now(); // Log request console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); console.log("📨 INCOMING REQUEST"); console.log("Time:", new Date().toISOString()); console.log("Method:", ctx.method); console.log("URL:", ctx.url); if (ctx.request.body && Object.keys(ctx.request.body).length > 0) { console.log("Body:", JSON.stringify(ctx.request.body, null, 2)); } try { await next(); // Log successful response const ms = Date.now() - start; console.log("\n✅ RESPONSE SUCCESS"); console.log("Status:", ctx.status); console.log("Time:", ms + "ms"); console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); } catch (err) { const ms = Date.now() - start; // Log error dengan detail lengkap console.log("\n❌ ERROR OCCURRED"); console.log("Time:", new Date().toISOString()); console.log("Duration:", ms + "ms"); console.log("Method:", ctx.method); console.log("URL:", ctx.url); console.log("Status:", err.status || err.statusCode || 500); console.log("Error Name:", err.name); console.log("Error Message:", err.message); if (err.errors) { console.log("Validation Errors:", JSON.stringify(err.errors, null, 2)); } console.log("\n📚 STACK TRACE:"); console.log(err.stack); console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); // Set response untuk client ctx.status = err.status || err.statusCode || 500; ctx.body = { success: false, error: { message: err.message || "Internal Server Error", status: ctx.status, timestamp: new Date().toISOString(), ...(err.errors && { details: err.errors }), }, }; // Emit error event ctx.app.emit("error", err, ctx); } }); app.use(logger()); app.use(responseTime()); app.use( bodyParser({ onerror: (err, ctx) => { console.error("\n⚠️ BODY PARSER ERROR"); console.error("Message:", err.message); console.error("Stack:", err.stack); console.error("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); ctx.throw(422, "Invalid JSON body"); }, }) ); // Tambahkan middleware untuk log response body pada error app.use(async (ctx, next) => { await next(); // Log response body jika status error (4xx atau 5xx) if (ctx.status >= 400) { console.log("\n⚠️ ERROR RESPONSE BODY:"); console.log(JSON.stringify(ctx.body, null, 2)); console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); } }); app.use(require("./helpers").helpersApi); const route = new Router(); const routes = require("./routes"); // Helper untuk format memory function formatBytes(bytes) { const sizes = ["B", "KB", "MB", "GB", "TB"]; if (bytes === 0) return "0 B"; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`; } function formatUptime(seconds) { const days = Math.floor(seconds / (24 * 3600)); seconds %= 24 * 3600; const hours = Math.floor(seconds / 3600); seconds %= 3600; const minutes = Math.floor(seconds / 60); seconds = Math.floor(seconds % 60); const parts = []; if (days) parts.push(`${days} day${days > 1 ? "s" : ""}`); if (hours) parts.push(`${hours} hour${hours > 1 ? "s" : ""}`); if (minutes) parts.push(`${minutes} minute${minutes > 1 ? "s" : ""}`); if (seconds || parts.length === 0) parts.push(`${seconds} second${seconds > 1 ? "s" : ""}`); return parts.join(" "); } route.get(["/", "/ping"], async (ctx) => { const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; const usagePercent = ((usedMem / totalMem) * 100).toFixed(2); const info = { hostname: os.hostname(), platform: os.platform(), arch: os.arch(), uptime: formatUptime(os.uptime()), cpu: { model: os.cpus()[0].model, cores: os.cpus().length, }, memory: { total: formatBytes(totalMem), used: formatBytes(usedMem), free: formatBytes(freeMem), usage: `${usagePercent}%`, }, }; ctx.type = "application/json"; ctx.body = JSON.stringify(info, null, 2); }); route.use("/*", routes.routeApi.routes()); app.use(route.routes()); // Global error event listener app.on("error", (err, ctx) => { // Additional logging bisa ditambahkan di sini // Misal: kirim ke Sentry, CloudWatch, dll if (process.env.DEBUG) { console.log("🔍 Additional error context:", { url: ctx.url, method: ctx.method, status: ctx.status, headers: ctx.headers, }); } }); const port = process.env.PORT || 7860; // Start server dengan download assets dulu async function startServer() { try { // Download assets dulu sebelum start server await ensureAssets(); // Baru start server app.listen(port, () => { console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); console.log("✅ Server started successfully"); console.log("📍 Listening on localhost, port", port); console.log("🌐 Health check: http://localhost:" + port + "/ping"); console.log("🐛 Debug mode:", process.env.DEBUG ? "ON" : "OFF"); console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); }); } catch (err) { console.error("❌ Failed to start server:", err.message); process.exit(1); } } // Jalankan server startServer(); // Handle unhandled promise rejections process.on("unhandledRejection", (reason, promise) => { console.error("\n🔥 Unhandled Rejection at:", promise); console.error("Reason:", reason); console.error("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); }); // Handle uncaught exceptions process.on("uncaughtException", (err) => { console.error("\n💥 Uncaught Exception:"); console.error(err); console.error("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); process.exit(1); });