2024-08-21 12:35:03 -03:00
|
|
|
import "dotenv/config";
|
2024-12-11 19:46:11 -03:00
|
|
|
import "./services/sentry";
|
2024-08-21 17:58:27 +02:00
|
|
|
import * as Sentry from "@sentry/node";
|
2024-08-23 17:29:42 +02:00
|
|
|
import express, { NextFunction, Request, Response } from "express";
|
2024-04-15 17:01:47 -04:00
|
|
|
import bodyParser from "body-parser";
|
|
|
|
import cors from "cors";
|
2025-01-07 16:24:46 -03:00
|
|
|
import { getExtractQueue, getScrapeQueue } from "./services/queue-service";
|
2024-04-20 16:38:05 -07:00
|
|
|
import { v0Router } from "./routes/v0";
|
2024-06-12 17:53:04 -07:00
|
|
|
import os from "os";
|
2024-11-07 20:57:33 +01:00
|
|
|
import { logger } from "./lib/logger";
|
2024-07-25 14:54:20 -04:00
|
|
|
import { adminRouter } from "./routes/admin";
|
2024-12-11 19:46:11 -03:00
|
|
|
import http from "node:http";
|
|
|
|
import https from "node:https";
|
|
|
|
import CacheableLookup from "cacheable-lookup";
|
2024-08-06 15:24:45 -03:00
|
|
|
import { v1Router } from "./routes/v1";
|
2024-08-17 01:04:14 +02:00
|
|
|
import expressWs from "express-ws";
|
2024-08-23 17:29:42 +02:00
|
|
|
import { ErrorResponse, ResponseWithSentry } from "./controllers/v1/types";
|
|
|
|
import { ZodError } from "zod";
|
|
|
|
import { v4 as uuidv4 } from "uuid";
|
2024-08-21 12:35:03 -03:00
|
|
|
|
2024-04-15 17:01:47 -04:00
|
|
|
const { createBullBoard } = require("@bull-board/api");
|
|
|
|
const { BullAdapter } = require("@bull-board/api/bullAdapter");
|
|
|
|
const { ExpressAdapter } = require("@bull-board/express");
|
|
|
|
|
2024-06-12 18:07:05 -07:00
|
|
|
const numCPUs = process.env.ENV === "local" ? 2 : os.cpus().length;
|
2024-11-07 20:57:33 +01:00
|
|
|
logger.info(`Number of CPUs: ${numCPUs} available`);
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-12-11 19:46:11 -03:00
|
|
|
const cacheable = new CacheableLookup();
|
2024-07-29 18:31:43 -04:00
|
|
|
|
2024-10-12 19:36:49 -03:00
|
|
|
// Install cacheable lookup for all other requests
|
2024-07-29 18:31:43 -04:00
|
|
|
cacheable.install(http.globalAgent);
|
2024-10-12 19:36:49 -03:00
|
|
|
cacheable.install(https.globalAgent);
|
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
const ws = expressWs(express());
|
|
|
|
const app = ws.app;
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
global.isProduction = process.env.IS_PRODUCTION === "true";
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
app.use(bodyParser.urlencoded({ extended: true }));
|
|
|
|
app.use(bodyParser.json({ limit: "10mb" }));
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
app.use(cors()); // Add this line to enable CORS
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
const serverAdapter = new ExpressAdapter();
|
|
|
|
serverAdapter.setBasePath(`/admin/${process.env.BULL_AUTH_KEY}/queues`);
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
const { addQueue, removeQueue, setQueues, replaceQueues } = createBullBoard({
|
2025-01-07 16:24:46 -03:00
|
|
|
queues: [new BullAdapter(getScrapeQueue()), new BullAdapter(getExtractQueue())],
|
2024-12-11 19:51:08 -03:00
|
|
|
serverAdapter: serverAdapter,
|
2024-10-09 23:13:26 +02:00
|
|
|
});
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
app.use(
|
|
|
|
`/admin/${process.env.BULL_AUTH_KEY}/queues`,
|
2024-12-11 19:51:08 -03:00
|
|
|
serverAdapter.getRouter(),
|
2024-10-09 23:13:26 +02:00
|
|
|
);
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
app.get("/", (req, res) => {
|
2024-10-09 19:30:14 -03:00
|
|
|
res.send("SCRAPERS-JS: Hello, world! K8s!");
|
2024-10-09 23:13:26 +02:00
|
|
|
});
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
//write a simple test function
|
|
|
|
app.get("/test", async (req, res) => {
|
|
|
|
res.send("Hello, world!");
|
|
|
|
});
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
// register router
|
|
|
|
app.use(v0Router);
|
|
|
|
app.use("/v1", v1Router);
|
|
|
|
app.use(adminRouter);
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
const DEFAULT_PORT = process.env.PORT ?? 3002;
|
|
|
|
const HOST = process.env.HOST ?? "localhost";
|
2024-05-20 13:36:34 -07:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
function startServer(port = DEFAULT_PORT) {
|
|
|
|
const server = app.listen(Number(port), HOST, () => {
|
2024-11-07 20:57:33 +01:00
|
|
|
logger.info(`Worker ${process.pid} listening on port ${port}`);
|
|
|
|
logger.info(
|
2024-12-11 19:51:08 -03:00
|
|
|
`For the Queue UI, open: http://${HOST}:${port}/admin/${process.env.BULL_AUTH_KEY}/queues`,
|
2024-10-09 23:13:26 +02:00
|
|
|
);
|
|
|
|
});
|
2024-11-12 18:10:24 +01:00
|
|
|
|
|
|
|
const exitHandler = () => {
|
2024-12-11 19:46:11 -03:00
|
|
|
logger.info("SIGTERM signal received: closing HTTP server");
|
2024-11-12 18:10:24 +01:00
|
|
|
server.close(() => {
|
|
|
|
logger.info("Server closed.");
|
|
|
|
process.exit(0);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2024-12-11 19:46:11 -03:00
|
|
|
process.on("SIGTERM", exitHandler);
|
|
|
|
process.on("SIGINT", exitHandler);
|
2024-10-09 23:13:26 +02:00
|
|
|
return server;
|
|
|
|
}
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
if (require.main === module) {
|
|
|
|
startServer();
|
|
|
|
}
|
2024-04-15 17:01:47 -04:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
app.get(`/serverHealthCheck`, async (req, res) => {
|
|
|
|
try {
|
|
|
|
const scrapeQueue = getScrapeQueue();
|
2024-12-11 19:46:11 -03:00
|
|
|
const [waitingJobs] = await Promise.all([scrapeQueue.getWaitingCount()]);
|
2024-10-09 23:13:26 +02:00
|
|
|
const noWaitingJobs = waitingJobs === 0;
|
|
|
|
// 200 if no active jobs, 503 if there are active jobs
|
|
|
|
return res.status(noWaitingJobs ? 200 : 500).json({
|
2024-12-11 19:51:08 -03:00
|
|
|
waitingJobs,
|
2024-04-15 17:01:47 -04:00
|
|
|
});
|
2024-10-09 23:13:26 +02:00
|
|
|
} catch (error) {
|
|
|
|
Sentry.captureException(error);
|
2024-11-07 20:57:33 +01:00
|
|
|
logger.error(error);
|
2024-10-09 23:13:26 +02:00
|
|
|
return res.status(500).json({ error: error.message });
|
2024-04-15 17:01:47 -04:00
|
|
|
}
|
2024-10-09 23:13:26 +02:00
|
|
|
});
|
2024-06-12 17:53:04 -07:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
app.get("/serverHealthCheck/notify", async (req, res) => {
|
|
|
|
if (process.env.SLACK_WEBHOOK_URL) {
|
|
|
|
const treshold = 1; // The treshold value for the active jobs
|
|
|
|
const timeout = 60000; // 1 minute // The timeout value for the check in milliseconds
|
2024-04-23 15:46:29 -03:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
const getWaitingJobsCount = async () => {
|
2024-08-13 21:03:24 +02:00
|
|
|
const scrapeQueue = getScrapeQueue();
|
2024-10-09 23:13:26 +02:00
|
|
|
const [waitingJobsCount] = await Promise.all([
|
2024-12-11 19:51:08 -03:00
|
|
|
scrapeQueue.getWaitingCount(),
|
2024-09-09 12:27:55 -03:00
|
|
|
]);
|
2024-04-23 15:46:29 -03:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
return waitingJobsCount;
|
|
|
|
};
|
|
|
|
|
|
|
|
res.status(200).json({ message: "Check initiated" });
|
|
|
|
|
|
|
|
const checkWaitingJobs = async () => {
|
|
|
|
try {
|
|
|
|
let waitingJobsCount = await getWaitingJobsCount();
|
|
|
|
if (waitingJobsCount >= treshold) {
|
|
|
|
setTimeout(async () => {
|
|
|
|
// Re-check the waiting jobs count after the timeout
|
|
|
|
waitingJobsCount = await getWaitingJobsCount();
|
|
|
|
if (waitingJobsCount >= treshold) {
|
2024-11-07 20:57:33 +01:00
|
|
|
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL!;
|
2024-10-09 23:13:26 +02:00
|
|
|
const message = {
|
|
|
|
text: `⚠️ Warning: The number of active jobs (${waitingJobsCount}) has exceeded the threshold (${treshold}) for more than ${
|
|
|
|
timeout / 60000
|
2024-12-11 19:51:08 -03:00
|
|
|
} minute(s).`,
|
2024-10-09 23:13:26 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const response = await fetch(slackWebhookUrl, {
|
|
|
|
method: "POST",
|
|
|
|
headers: {
|
2024-12-11 19:51:08 -03:00
|
|
|
"Content-Type": "application/json",
|
2024-10-09 23:13:26 +02:00
|
|
|
},
|
2024-12-11 19:51:08 -03:00
|
|
|
body: JSON.stringify(message),
|
2024-10-09 23:13:26 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2024-11-07 20:57:33 +01:00
|
|
|
logger.error("Failed to send Slack notification");
|
2024-04-23 15:46:29 -03:00
|
|
|
}
|
2024-10-09 23:13:26 +02:00
|
|
|
}
|
|
|
|
}, timeout);
|
2024-04-23 15:46:29 -03:00
|
|
|
}
|
2024-10-09 23:13:26 +02:00
|
|
|
} catch (error) {
|
|
|
|
Sentry.captureException(error);
|
2024-11-07 20:57:33 +01:00
|
|
|
logger.debug(error);
|
2024-10-09 23:13:26 +02:00
|
|
|
}
|
|
|
|
};
|
2024-04-23 15:46:29 -03:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
checkWaitingJobs();
|
|
|
|
}
|
|
|
|
});
|
2024-06-12 17:53:04 -07:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
app.get("/is-production", (req, res) => {
|
|
|
|
res.send({ isProduction: global.isProduction });
|
|
|
|
});
|
2024-05-30 14:31:36 -07:00
|
|
|
|
2024-12-11 19:46:11 -03:00
|
|
|
app.use(
|
|
|
|
(
|
|
|
|
err: unknown,
|
|
|
|
req: Request<{}, ErrorResponse, undefined>,
|
|
|
|
res: Response<ErrorResponse>,
|
2024-12-11 19:51:08 -03:00
|
|
|
next: NextFunction,
|
2024-12-11 19:46:11 -03:00
|
|
|
) => {
|
|
|
|
if (err instanceof ZodError) {
|
|
|
|
if (
|
|
|
|
Array.isArray(err.errors) &&
|
|
|
|
err.errors.find((x) => x.message === "URL uses unsupported protocol")
|
|
|
|
) {
|
2024-11-07 20:57:33 +01:00
|
|
|
logger.warn("Unsupported protocol error: " + JSON.stringify(req.body));
|
2024-10-09 23:13:26 +02:00
|
|
|
}
|
2024-10-01 22:13:28 +02:00
|
|
|
|
2024-12-11 19:46:11 -03:00
|
|
|
res
|
|
|
|
.status(400)
|
|
|
|
.json({ success: false, error: "Bad Request", details: err.errors });
|
|
|
|
} else {
|
2024-10-09 23:13:26 +02:00
|
|
|
next(err);
|
2024-12-11 19:46:11 -03:00
|
|
|
}
|
2024-12-11 19:51:08 -03:00
|
|
|
},
|
2024-12-11 19:46:11 -03:00
|
|
|
);
|
2024-08-30 17:33:42 +02:00
|
|
|
|
2024-10-09 23:13:26 +02:00
|
|
|
Sentry.setupExpressErrorHandler(app);
|
2024-08-23 17:29:42 +02:00
|
|
|
|
2024-12-11 19:46:11 -03:00
|
|
|
app.use(
|
|
|
|
(
|
|
|
|
err: unknown,
|
|
|
|
req: Request<{}, ErrorResponse, undefined>,
|
|
|
|
res: ResponseWithSentry<ErrorResponse>,
|
2024-12-11 19:51:08 -03:00
|
|
|
next: NextFunction,
|
2024-12-11 19:46:11 -03:00
|
|
|
) => {
|
|
|
|
if (
|
|
|
|
err instanceof SyntaxError &&
|
|
|
|
"status" in err &&
|
|
|
|
err.status === 400 &&
|
|
|
|
"body" in err
|
|
|
|
) {
|
|
|
|
return res
|
|
|
|
.status(400)
|
|
|
|
.json({ success: false, error: "Bad request, malformed JSON" });
|
|
|
|
}
|
2024-09-09 12:26:55 -03:00
|
|
|
|
2024-12-11 19:46:11 -03:00
|
|
|
const id = res.sentry ?? uuidv4();
|
|
|
|
let verbose = JSON.stringify(err);
|
|
|
|
if (verbose === "{}") {
|
|
|
|
if (err instanceof Error) {
|
|
|
|
verbose = JSON.stringify({
|
|
|
|
message: err.message,
|
|
|
|
name: err.name,
|
2024-12-11 19:51:08 -03:00
|
|
|
stack: err.stack,
|
2024-12-11 19:46:11 -03:00
|
|
|
});
|
|
|
|
}
|
2024-08-23 17:29:42 +02:00
|
|
|
}
|
2024-09-09 12:27:55 -03:00
|
|
|
|
2024-12-11 19:46:11 -03:00
|
|
|
logger.error(
|
|
|
|
"Error occurred in request! (" +
|
|
|
|
req.path +
|
|
|
|
") -- ID " +
|
|
|
|
id +
|
|
|
|
" -- " +
|
2024-12-11 19:51:08 -03:00
|
|
|
verbose,
|
2024-12-11 19:46:11 -03:00
|
|
|
);
|
2024-12-11 19:48:22 -03:00
|
|
|
res.status(500).json({
|
|
|
|
success: false,
|
|
|
|
error:
|
|
|
|
"An unexpected error occurred. Please contact help@firecrawl.com for help. Your exception ID is " +
|
2024-12-11 19:51:08 -03:00
|
|
|
id,
|
2024-12-11 19:48:22 -03:00
|
|
|
});
|
2024-12-11 19:51:08 -03:00
|
|
|
},
|
2024-12-11 19:46:11 -03:00
|
|
|
);
|
2024-09-09 12:27:55 -03:00
|
|
|
|
2024-11-07 20:57:33 +01:00
|
|
|
logger.info(`Worker ${process.pid} started`);
|
2024-09-09 12:27:55 -03:00
|
|
|
|
2024-08-13 21:03:24 +02:00
|
|
|
// const sq = getScrapeQueue();
|
2024-07-30 13:27:23 -04:00
|
|
|
|
2024-08-13 21:03:24 +02:00
|
|
|
// sq.on("waiting", j => ScrapeEvents.logJobEvent(j, "waiting"));
|
|
|
|
// sq.on("active", j => ScrapeEvents.logJobEvent(j, "active"));
|
|
|
|
// sq.on("completed", j => ScrapeEvents.logJobEvent(j, "completed"));
|
|
|
|
// sq.on("paused", j => ScrapeEvents.logJobEvent(j, "paused"));
|
|
|
|
// sq.on("resumed", j => ScrapeEvents.logJobEvent(j, "resumed"));
|
|
|
|
// sq.on("removed", j => ScrapeEvents.logJobEvent(j, "removed"));
|
2024-12-30 20:04:22 -03:00
|
|
|
//
|