diff --git a/scrapers/cpex-scraper/src/index.ts b/scrapers/cpex-scraper/src/index.ts index 71e8f5a50a..760b2e9d07 100644 --- a/scrapers/cpex-scraper/src/index.ts +++ b/scrapers/cpex-scraper/src/index.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { createFileLogger } from './logger'; import { scrapeCPEx, type ScraperEnv } from './scraper'; const ACADEMIC_YEAR = '2026/27'; @@ -12,10 +13,17 @@ const threshold = 1500; const envPath = path.join(__dirname, '../../env.json'); const env = JSON.parse(fs.readFileSync(envPath, 'utf8')) as ScraperEnv; +const logger = createFileLogger(); + scrapeCPEx({ academicYear: ACADEMIC_YEAR, env, + logger, threshold, -}).catch((error) => { - console.error(`Failed to scrape: ${error}`); +}).then(async () => { + await logger.close(); +}).catch(async (error) => { + logger.log(`Failed to scrape: ${error}`); + await logger.close(); + process.exitCode = 1; }); diff --git a/scrapers/cpex-scraper/src/logger.ts b/scrapers/cpex-scraper/src/logger.ts new file mode 100644 index 0000000000..fe340ae242 --- /dev/null +++ b/scrapers/cpex-scraper/src/logger.ts @@ -0,0 +1,67 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +// At runtime, __dirname is build/src/ (compiled from src/). +// Two levels up reaches the scraper root: build/src/ -> build/ -> scrapers/cpex-scraper/ +const LOG_DIR = path.join(__dirname, '../../logs'); + +function pad2(n: number): string { + return n < 10 ? `0${n}` : String(n); +} + +function formatTimestamp(date: Date): string { + return ( + date.getFullYear().toString() + + '-' + + pad2(date.getMonth() + 1) + + '-' + + pad2(date.getDate()) + + '.' + + pad2(date.getHours()) + + '-' + + pad2(date.getMinutes()) + + '-' + + pad2(date.getSeconds()) + ); +} + +export type FileLogger = Pick & { + close: () => Promise; +}; + +/** + * Creates a logger that writes timestamped lines to both stdout and a log file. + * The log file is written to `scrapers/cpex-scraper/logs/cpex-YYYY-MM-DD.HH-mm-ss.log`. + * + * Satisfies the `Pick` interface used by the CPEx scraper. + */ +export function createFileLogger(now: Date = new Date()): FileLogger { + if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }); + } + + const filename = `cpex-${formatTimestamp(now)}.log`; + const logPath = path.join(LOG_DIR, filename); + const stream = fs.createWriteStream(logPath, { flags: 'a' }); + + stream.on('error', (err) => { + console.error(`[FileLogger] write stream error: ${err.message}`); + }); + + function log(...args: Array): void { + const timestamp = new Date().toISOString(); + const message = args.map((a) => (typeof a === 'string' ? a : String(a))).join(' '); + const line = `[${timestamp}] ${message}`; + + console.log(line); + stream.write(`${line}\n`); + } + + function close(): Promise { + return new Promise((resolve, reject) => { + stream.end((err?: Error | null) => (err ? reject(err) : resolve())); + }); + } + + return { close, log }; +}