feat: Add Pino structured logging, release-please CI/CD and Docker pipeline

Replace all console.log/error with Pino logger (info/warn/error/debug/fatal)
for structured JSON logging in production and pretty-print in development.
Remove redundant header dumps and consolidate rate-limit logging.

Add GitHub Actions workflow with release-please for automated semver releases
and Docker build/push to GHCR on new releases.
This commit is contained in:
Samuel Enocsson
2026-02-21 15:56:57 +01:00
parent 371a398446
commit 6ac32457a9
14 changed files with 498 additions and 189 deletions
+27 -26
View File
@@ -4,6 +4,7 @@ const { db } = require('../db');
const { getAllCoursesFromDB, getLayoutsForCourse, updateLayoutRating } = require('../models/course');
const { launchBrowser } = require('../scrapers/browser');
const { layoutEventCache, scrapeCourseDirectory, scrapeCourseLayouts, scrapeEventResults } = require('../scrapers/course-puppeteer');
const logger = require('../logger');
// Request locking to prevent concurrent scrapes of the same resource
const activeScrapes = new Map();
@@ -33,7 +34,7 @@ router.get('/partials/course-layouts/:courseId', async (req, res) => {
const layouts = await getLayoutsForCourse(courseId);
res.render('../partials/course-layouts', { layouts, courseId });
} catch (error) {
console.error('Error loading course layouts:', error.message);
logger.error('Error loading course layouts:', error.message);
res.status(500).send('<div class="no-layouts">Error loading layouts</div>');
}
});
@@ -43,7 +44,7 @@ router.get('/api/courses', async (req, res) => {
const courses = await getAllCoursesFromDB();
res.json(courses);
} catch (error) {
console.error('Error fetching courses:', error.message);
logger.error('Error fetching courses:', error.message);
res.status(500).json({ error: 'Failed to fetch courses' });
}
});
@@ -54,7 +55,7 @@ router.get('/api/layouts/:courseId', async (req, res) => {
const layouts = await getLayoutsForCourse(courseId);
res.json(layouts);
} catch (error) {
console.error('Error fetching layouts:', error.message);
logger.error('Error fetching layouts:', error.message);
res.status(500).json({ error: 'Failed to fetch layouts' });
}
});
@@ -65,7 +66,7 @@ router.post('/api/scrape-courses', async (req, res) => {
let browser = null;
try {
console.log('Starting course directory scraping...');
logger.info('Starting course directory scraping...');
browser = await launchBrowser();
@@ -80,7 +81,7 @@ router.post('/api/scrape-courses', async (req, res) => {
message: `Successfully scraped ${courses.length} courses`
});
} catch (error) {
console.error('Error scraping courses:', error.message);
logger.error('Error scraping courses:', error.message);
if (browser) {
try { await browser.close(); } catch (e) {}
}
@@ -96,7 +97,7 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
const lockKey = `layout-${courseId}`;
if (activeScrapes.has(lockKey)) {
console.log(`⚠️ Scrape already in progress for course ${courseId}`);
logger.info(`⚠️ Scrape already in progress for course ${courseId}`);
return res.status(409).json({
error: 'Scrape already in progress for this course',
message: 'Please wait for the current scrape to complete'
@@ -118,19 +119,19 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
throw new Error('Course not found');
}
console.log(`Starting layout scraping for course: ${course.name}`);
logger.info(`Starting layout scraping for course: ${course.name}`);
browser = await launchBrowser();
const layouts = await scrapeCourseLayouts(browser, course.link, courseId);
console.log(`\n=== Starting event results scraping for ${course.name} ===`);
logger.info(`\n=== Starting event results scraping for ${course.name} ===`);
const courseIdInt = parseInt(courseId);
const layoutData = layoutEventCache.get(courseIdInt);
if (!layoutData || layoutData.length === 0) {
console.log('No event data found in cache, skipping event results scraping');
logger.info('No event data found in cache, skipping event results scraping');
await browser.close();
browser = null;
@@ -183,7 +184,7 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.log(`\n=== Calculating final ratings for all layouts ===`);
logger.info(`\n=== Calculating final ratings for all layouts ===`);
let savedCount = 0;
for (const layoutKey in allLayoutRatings) {
@@ -194,10 +195,10 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
layoutDataResult.allRatings.reduce((sum, r) => sum + r, 0) / layoutDataResult.allRatings.length
);
console.log(`Layout: ${layoutDataResult.name} (Par ${layoutDataResult.par})`);
console.log(` Total ratings collected: ${layoutDataResult.allRatings.length}`);
console.log(` Mean rating: ${meanRating}`);
console.log(` Last played: ${layoutDataResult.latestDate || 'Unknown'}`);
logger.debug(`Layout: ${layoutDataResult.name} (Par ${layoutDataResult.par})`);
logger.debug(` Total ratings collected: ${layoutDataResult.allRatings.length}`);
logger.debug(` Mean rating: ${meanRating}`);
logger.debug(` Last played: ${layoutDataResult.latestDate || 'Unknown'}`);
try {
const changes = await updateLayoutRating(
@@ -209,11 +210,11 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
layoutDataResult.latestDate
);
if (changes > 0) {
console.log(` ✓ Updated in database`);
logger.info(` ✓ Updated in database`);
savedCount++;
}
} catch (err) {
console.error(` Error updating layout ${layoutDataResult.name}:`, err.message);
logger.error(` Error updating layout ${layoutDataResult.name}:`, err.message);
}
}
}
@@ -229,7 +230,7 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
message: `Successfully scraped ${layouts.length} layouts and processed ${Object.keys(eventGroups).length} events for ${course.name}`
};
} catch (error) {
console.error('Error scraping layouts:', error.message);
logger.error('Error scraping layouts:', error.message);
if (browser) {
try { await browser.close(); } catch (e) {}
}
@@ -249,7 +250,7 @@ router.post('/api/scrape-layouts/:courseId', async (req, res) => {
});
} finally {
activeScrapes.delete(lockKey);
console.log(`✓ Released lock for course ${courseId}`);
logger.info(`✓ Released lock for course ${courseId}`);
}
});
@@ -317,7 +318,7 @@ router.post('/api/scrape-event-results/:courseId', async (req, res) => {
await browser.close();
browser = null;
console.log(`\n=== Calculating final ratings for all layouts ===`);
logger.info(`\n=== Calculating final ratings for all layouts ===`);
let savedCount = 0;
for (const layoutKey in allLayoutRatings) {
@@ -328,10 +329,10 @@ router.post('/api/scrape-event-results/:courseId', async (req, res) => {
ld.allRatings.reduce((sum, r) => sum + r, 0) / ld.allRatings.length
);
console.log(`Layout: ${ld.name} (Par ${ld.par})`);
console.log(` Total ratings collected: ${ld.allRatings.length}`);
console.log(` Mean rating: ${meanRating}`);
console.log(` Last played: ${ld.latestDate || 'Unknown'}`);
logger.debug(`Layout: ${ld.name} (Par ${ld.par})`);
logger.debug(` Total ratings collected: ${ld.allRatings.length}`);
logger.debug(` Mean rating: ${meanRating}`);
logger.debug(` Last played: ${ld.latestDate || 'Unknown'}`);
try {
const changes = await updateLayoutRating(
@@ -343,11 +344,11 @@ router.post('/api/scrape-event-results/:courseId', async (req, res) => {
ld.latestDate
);
if (changes > 0) {
console.log(` ✓ Updated in database`);
logger.info(` ✓ Updated in database`);
savedCount++;
}
} catch (err) {
console.error(` Error updating layout ${ld.name}:`, err.message);
logger.error(` Error updating layout ${ld.name}:`, err.message);
}
}
}
@@ -360,7 +361,7 @@ router.post('/api/scrape-event-results/:courseId', async (req, res) => {
message: `Processed ${Object.keys(eventGroups).length} events, updated ${savedCount} layouts`
});
} catch (error) {
console.error('Error scraping event results:', error.message);
logger.error('Error scraping event results:', error.message);
if (browser) {
try { await browser.close(); } catch (e) {}
}