firecrawl/apps/api/src/index.ts

193 lines
6.0 KiB
TypeScript
Raw Normal View History

2024-04-15 17:01:47 -04:00
import express from "express";
import bodyParser from "body-parser";
import cors from "cors";
import "dotenv/config";
2024-07-30 14:44:13 -04:00
import { getScrapeQueue, getWebScraperQueue } from "./services/queue-service";
2024-04-20 16:38:05 -07:00
import { v0Router } from "./routes/v0";
2024-07-11 23:20:51 +02:00
import { initSDK } from "@hyperdx/node-opentelemetry";
2024-06-12 17:53:04 -07:00
import cluster from "cluster";
import os from "os";
2024-07-23 17:30:46 -03:00
import { Logger } from "./lib/logger";
2024-07-25 14:54:20 -04:00
import { adminRouter } from "./routes/admin";
2024-07-24 18:44:14 +02:00
import { ScrapeEvents } from "./lib/scrape-events";
2024-07-29 18:31:43 -04:00
import http from 'node:http';
import https from 'node:https';
import CacheableLookup from 'cacheable-lookup';
2024-05-20 13:36:34 -07: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-07-23 17:30:46 -03:00
Logger.info(`Number of CPUs: ${numCPUs} available`);
2024-04-15 17:01:47 -04:00
2024-07-29 18:31:43 -04:00
const cacheable = new CacheableLookup({
// this is important to avoid querying local hostnames see https://github.com/szmarczak/cacheable-lookup readme
lookup:false
});
cacheable.install(http.globalAgent);
cacheable.install(https.globalAgent)
2024-06-12 17:53:04 -07:00
if (cluster.isMaster) {
2024-07-23 17:30:46 -03:00
Logger.info(`Master ${process.pid} is running`);
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
cluster.on("exit", (worker, code, signal) => {
2024-07-09 14:29:32 +02:00
if (code !== null) {
2024-07-23 17:30:46 -03:00
Logger.info(`Worker ${worker.process.pid} exited`);
Logger.info("Starting a new worker");
2024-07-09 14:29:32 +02:00
cluster.fork();
}
2024-06-12 17:53:04 -07:00
});
} else {
const app = express();
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
global.isProduction = process.env.IS_PRODUCTION === "true";
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json({ limit: "10mb" }));
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
app.use(cors()); // Add this line to enable CORS
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
const serverAdapter = new ExpressAdapter();
serverAdapter.setBasePath(`/admin/${process.env.BULL_AUTH_KEY}/queues`);
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
const { addQueue, removeQueue, setQueues, replaceQueues } = createBullBoard({
2024-07-30 14:44:13 -04:00
queues: [new BullAdapter(getWebScraperQueue()), new BullAdapter(getScrapeQueue())],
2024-06-12 17:53:04 -07:00
serverAdapter: serverAdapter,
});
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
app.use(
`/admin/${process.env.BULL_AUTH_KEY}/queues`,
serverAdapter.getRouter()
);
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
app.get("/", (req, res) => {
res.send("SCRAPERS-JS: Hello, world! Fly.io");
});
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
//write a simple test function
app.get("/test", async (req, res) => {
res.send("Hello, world!");
});
2024-05-20 13:36:34 -07:00
2024-06-12 17:53:04 -07:00
// register router
app.use(v0Router);
2024-07-25 14:54:20 -04:00
app.use(adminRouter);
2024-04-21 11:27:31 -07:00
2024-06-12 17:53:04 -07:00
const DEFAULT_PORT = process.env.PORT ?? 3002;
const HOST = process.env.HOST ?? "localhost";
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
// HyperDX OpenTelemetry
2024-07-11 23:20:51 +02:00
if (process.env.ENV === "production") {
initSDK({ consoleCapture: true, additionalInstrumentations: [] });
}
2024-04-15 17:01:47 -04:00
2024-06-12 17:53:04 -07:00
function startServer(port = DEFAULT_PORT) {
const server = app.listen(Number(port), HOST, () => {
2024-07-23 17:30:46 -03:00
Logger.info(`Worker ${process.pid} listening on port ${port}`);
2024-07-25 09:48:06 -03:00
Logger.info(
`For the Queue UI, open: http://${HOST}:${port}/admin/${process.env.BULL_AUTH_KEY}/queues`
);
2024-04-15 17:01:47 -04:00
});
2024-06-12 17:53:04 -07:00
return server;
2024-04-15 17:01:47 -04:00
}
2024-06-12 17:53:04 -07:00
if (require.main === module) {
startServer();
}
2024-06-12 17:53:04 -07:00
app.get(`/serverHealthCheck`, async (req, res) => {
try {
const webScraperQueue = getWebScraperQueue();
2024-06-12 17:53:04 -07:00
const [waitingJobs] = await Promise.all([
2024-04-23 16:07:22 -03:00
webScraperQueue.getWaitingCount(),
]);
2024-06-12 17:53:04 -07:00
const noWaitingJobs = waitingJobs === 0;
// 200 if no active jobs, 503 if there are active jobs
return res.status(noWaitingJobs ? 200 : 500).json({
waitingJobs,
});
} catch (error) {
2024-07-23 17:30:46 -03:00
Logger.error(error);
2024-06-12 17:53:04 -07:00
return res.status(500).json({ error: error.message });
}
});
2024-06-12 17:53:04 -07: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
const getWaitingJobsCount = async () => {
const webScraperQueue = getWebScraperQueue();
const [waitingJobsCount] = await Promise.all([
webScraperQueue.getWaitingCount(),
]);
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) {
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
const message = {
text: `⚠️ Warning: The number of active jobs (${waitingJobsCount}) has exceeded the threshold (${treshold}) for more than ${
timeout / 60000
} minute(s).`,
};
const response = await fetch(slackWebhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(message),
});
if (!response.ok) {
2024-07-23 17:30:46 -03:00
Logger.error("Failed to send Slack notification");
2024-06-12 17:53:04 -07:00
}
}
2024-06-12 17:53:04 -07:00
}, timeout);
}
} catch (error) {
2024-07-23 17:30:46 -03:00
Logger.debug(error);
}
2024-06-12 17:53:04 -07:00
};
2024-06-12 17:53:04 -07:00
checkWaitingJobs();
}
});
app.get("/is-production", (req, res) => {
res.send({ isProduction: global.isProduction });
});
2024-05-30 14:31:36 -07:00
2024-07-23 17:30:46 -03:00
Logger.info(`Worker ${process.pid} started`);
2024-06-12 17:53:04 -07:00
}
2024-07-24 18:44:14 +02:00
2024-07-30 13:27:23 -04:00
// const wsq = getWebScraperQueue();
// wsq.on("waiting", j => ScrapeEvents.logJobEvent(j, "waiting"));
// wsq.on("active", j => ScrapeEvents.logJobEvent(j, "active"));
// wsq.on("completed", j => ScrapeEvents.logJobEvent(j, "completed"));
// wsq.on("paused", j => ScrapeEvents.logJobEvent(j, "paused"));
// wsq.on("resumed", j => ScrapeEvents.logJobEvent(j, "resumed"));
// wsq.on("removed", j => ScrapeEvents.logJobEvent(j, "removed"));
2024-08-12 15:07:30 -04:00