From c3325b8627ddd8950cd34ac3b762f98f4ff3e9f9 Mon Sep 17 00:00:00 2001 From: Gio Coia Date: Mon, 11 Aug 2025 15:51:07 -0700 Subject: [PATCH 1/3] Add production logging framework to app, db and lb --- .env.example | 4 + Dockerfile | 6 +- config/Caddyfile.http | 7 +- config/Caddyfile.https-production | 7 +- docker-compose.yml | 28 +++- package-lock.json | 216 +++++++++++++++++++++++++++++- package.json | 11 +- public/js/api.js | 22 +-- public/js/forms.js | 2 +- src/config/auth.js | 3 + src/index.js | 48 +++++-- src/routes/auth.js | 7 +- src/routes/leaderboard.js | 31 ++++- src/utils/appConfig.js | 2 +- src/utils/database.js | 10 +- src/utils/logger.js | 170 +++++++++++++++++++++++ 16 files changed, 533 insertions(+), 41 deletions(-) create mode 100644 src/utils/logger.js diff --git a/.env.example b/.env.example index 5caf94e..2c11304 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,10 @@ SESSION_SECRET=your_session_secret_here # Authorized email addresses (comma separated) AUTHORIZED_EMAILS=your_email@example.com,another_admin@example.com +# Logging Configuration +LOG_LEVEL=debug +NODE_ENV=production + # Production Configuration (for HTTPS production mode only) # Only needed when using Caddyfile.https-production DOMAIN=yourdomain.com diff --git a/Dockerfile b/Dockerfile index 87fa0fa..3cd2a1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,11 +7,11 @@ RUN apk add --no-cache curl # Set the working directory in the container WORKDIR /usr/src/app -# Copy package*.json files +# Copy package files for dependency installation COPY package*.json ./ -# Install dependencies -RUN npm ci --only=production +# Install dependencies (production only) +RUN npm ci --omit=dev && npm cache clean --force # Copy the rest of the application code COPY . . diff --git a/config/Caddyfile.http b/config/Caddyfile.http index 248551e..dc8bbde 100644 --- a/config/Caddyfile.http +++ b/config/Caddyfile.http @@ -24,7 +24,12 @@ http://localhost { encode gzip log { - output file /var/log/caddy/access.log + output file /var/log/caddy/access.log { + roll_size 10MB + roll_keep 14 + roll_keep_for 720h + } format json + level INFO } } \ No newline at end of file diff --git a/config/Caddyfile.https-production b/config/Caddyfile.https-production index f00ddb0..23989b6 100644 --- a/config/Caddyfile.https-production +++ b/config/Caddyfile.https-production @@ -27,7 +27,12 @@ encode gzip log { - output file /var/log/caddy/access.log + output file /var/log/caddy/access.log { + roll_size 10MB + roll_keep 14 + roll_keep_for 720h + } format json + level INFO } } diff --git a/docker-compose.yml b/docker-compose.yml index 38db76b..fa9cc13 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,9 @@ # Reusable configuration blocks x-healthcheck-defaults: &healthcheck-defaults - interval: 30s - timeout: 10s + interval: 10s + timeout: 5s retries: 3 + start_period: 30s services: db: @@ -13,9 +14,15 @@ services: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - db_data:/var/lib/postgresql/data - ports: - - 5432:5432 + # Uncomment for database debugging/access from host: + # ports: + # - 5432:5432 restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" healthcheck: <<: *healthcheck-defaults test: [CMD-SHELL, "pg_isready -U ${DB_USER} -d ${DB_NAME}"] @@ -35,12 +42,20 @@ services: - GOOGLE_CALLBACK_URL=${GOOGLE_CALLBACK_URL} - SESSION_SECRET=${SESSION_SECRET} - AUTHORIZED_EMAILS=${AUTHORIZED_EMAILS} + - LOG_LEVEL=${LOG_LEVEL:-info} + - NODE_ENV=${NODE_ENV:-production} volumes: - uploads_shared:/usr/src/app/public/uploads + - ./logs:/usr/src/app/logs depends_on: db: condition: service_healthy restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "20m" + max-file: "10" healthcheck: <<: *healthcheck-defaults test: [CMD-SHELL, "curl -f http://localhost:${PORT}/ || exit 1"] @@ -74,6 +89,11 @@ services: db: condition: service_healthy restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "7" networks: - app-network diff --git a/package-lock.json b/package-lock.json index d9d8b79..c71c68e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "passport-google-oauth20": "^2.0.0", "pg": "^8.16.3", "pg-hstore": "^2.3.4", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "winston": "^3.15.0" }, "devDependencies": { "@biomejs/biome": "^2.1.4", @@ -721,6 +722,26 @@ "node": ">=14.21.3" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", @@ -1443,6 +1464,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", @@ -1912,6 +1939,12 @@ "dev": true, "license": "MIT" }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2616,6 +2649,16 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2633,9 +2676,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -2647,6 +2699,31 @@ "color-support": "bin.js" } }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2966,6 +3043,12 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -3295,6 +3378,12 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -3346,6 +3435,12 @@ "node": ">=8" } }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4151,7 +4246,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4942,6 +5036,12 @@ "node": ">=6" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -4978,6 +5078,23 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5832,6 +5949,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -6625,6 +6751,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -6920,6 +7055,21 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -7085,6 +7235,15 @@ "license": "ISC", "optional": true }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -7484,6 +7643,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7529,6 +7694,15 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7870,6 +8044,42 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", diff --git a/package.json b/package.json index a0e8d96..ca69c9c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,13 @@ "format": "biome format .", "format:fix": "biome format . --write", "check": "biome check .", - "check:fix": "biome check . --fix" + "check:fix": "biome check . --fix", + "docker:up": "docker compose up -d --build", + "docker:dev": "docker compose up --build", + "docker:logs": "docker compose logs -f", + "logs:cleanup": "./scripts/cleanup-logs.sh", + "logs:view": "tail -f logs/combined.log", + "logs:size": "du -sh logs/ && docker system df" }, "keywords": [], "author": "", @@ -29,7 +35,8 @@ "passport-google-oauth20": "^2.0.0", "pg": "^8.16.3", "pg-hstore": "^2.3.4", - "sequelize": "^6.37.7" + "sequelize": "^6.37.7", + "winston": "^3.15.0" }, "devDependencies": { "@biomejs/biome": "^2.1.4", diff --git a/public/js/api.js b/public/js/api.js index feac133..311cd0f 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -24,7 +24,7 @@ export async function logoutUser() { // Leaderboard API calls export async function fetchLeaderboard() { - const response = await fetch(`${API_BASE_URL}/leaderboard`) + const response = await fetch(`${API_BASE_URL}/api/leaderboard`) if (!response.ok) { throw new Error('Failed to load leaderboard') } @@ -32,7 +32,7 @@ export async function fetchLeaderboard() { } export async function createDriver(driverData) { - const response = await fetch(`${API_BASE_URL}/leaderboard`, { + const response = await fetch(`${API_BASE_URL}/api/leaderboard`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -49,7 +49,7 @@ export async function createDriver(driverData) { } export async function updateDriver(driverId, driverData) { - const response = await fetch(`${API_BASE_URL}/leaderboard/${driverId}`, { + const response = await fetch(`${API_BASE_URL}/api/leaderboard/${driverId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -66,7 +66,7 @@ export async function updateDriver(driverId, driverData) { } export async function deleteDriver(driverId) { - const response = await fetch(`${API_BASE_URL}/leaderboard/${driverId}`, { + const response = await fetch(`${API_BASE_URL}/api/leaderboard/${driverId}`, { method: 'DELETE', }) @@ -83,7 +83,7 @@ export async function uploadProfilePicture(driverId, file) { const formData = new FormData() formData.append('profilePicture', file) - const response = await fetch(`${API_BASE_URL}/leaderboard/${driverId}/profile-picture`, { + const response = await fetch(`${API_BASE_URL}/api/leaderboard/${driverId}/profile-picture`, { method: 'POST', body: formData, }) @@ -97,7 +97,7 @@ export async function uploadProfilePicture(driverId, file) { } export async function deleteProfilePicture(driverId) { - const response = await fetch(`${API_BASE_URL}/leaderboard/${driverId}/profile-picture`, { + const response = await fetch(`${API_BASE_URL}/api/leaderboard/${driverId}/profile-picture`, { method: 'DELETE', }) @@ -111,7 +111,7 @@ export async function deleteProfilePicture(driverId) { // Race settings API calls export async function fetchRaceSettings() { - const response = await fetch(`${API_BASE_URL}/race-settings`) + const response = await fetch(`${API_BASE_URL}/api/race-settings`) if (!response.ok) { throw new Error('Failed to load race settings') } @@ -119,7 +119,7 @@ export async function fetchRaceSettings() { } export async function updateRaceSettings(settingsData) { - const response = await fetch(`${API_BASE_URL}/race-settings`, { + const response = await fetch(`${API_BASE_URL}/api/race-settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -139,7 +139,7 @@ export async function uploadCircuitImage(file) { const formData = new FormData() formData.append('circuitImage', file) - const response = await fetch(`${API_BASE_URL}/race-settings/circuit-image`, { + const response = await fetch(`${API_BASE_URL}/api/race-settings/circuit-image`, { method: 'POST', body: formData, }) @@ -153,7 +153,7 @@ export async function uploadCircuitImage(file) { } export async function deleteCircuitImage() { - const response = await fetch(`${API_BASE_URL}/race-settings/circuit-image`, { + const response = await fetch(`${API_BASE_URL}/api/race-settings/circuit-image`, { method: 'DELETE', }) @@ -166,7 +166,7 @@ export async function deleteCircuitImage() { } export async function clearNextRace() { - const response = await fetch(`${API_BASE_URL}/race-settings/clear-next-race`, { + const response = await fetch(`${API_BASE_URL}/api/race-settings/clear-next-race`, { method: 'POST', }) diff --git a/public/js/forms.js b/public/js/forms.js index 07c7963..a1e74a9 100644 --- a/public/js/forms.js +++ b/public/js/forms.js @@ -135,7 +135,7 @@ export async function deleteProfilePicture() { if (!driverId) return try { - const response = await fetch(`${API_BASE_URL}/leaderboard/${driverId}/profile-picture`, { + const response = await fetch(`${API_BASE_URL}/api/leaderboard/${driverId}/profile-picture`, { method: 'DELETE', }) diff --git a/src/config/auth.js b/src/config/auth.js index fbf9954..bd8cd3c 100644 --- a/src/config/auth.js +++ b/src/config/auth.js @@ -1,5 +1,6 @@ const passport = require('passport') const GoogleStrategy = require('passport-google-oauth20').Strategy +const logger = require('../utils/logger') // Configure Google OAuth strategy passport.use( @@ -19,8 +20,10 @@ passport.use( photo: profile.photos[0].value, } + logger.logAuth('login', user, true, { provider: 'Google' }) return done(null, user) } catch (error) { + logger.logAuth('login', null, false, { provider: 'Google', error: error.message }) return done(error, null) } } diff --git a/src/index.js b/src/index.js index 0bc9309..c7e6700 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ const { configureApp } = require('./utils/appConfig') const { initializeDatabase } = require('./utils/database') +const logger = require('./utils/logger') // Import route modules const authRoutes = require('./routes/auth') @@ -9,20 +10,49 @@ const raceSettingsRoutes = require('./routes/raceSettings') // Load environment variables require('dotenv').config() +logger.info('Starting Formula Evergreen Championship API...', { + nodeEnv: process.env.NODE_ENV || 'development', + port: process.env.PORT || 3000, +}) + const app = configureApp() const PORT = process.env.PORT || 3000 -// Root route -app.get('/', (_req, res) => { - res.json({ message: 'Formula Evergreen Championship API' }) -}) +// Add request logging middleware +app.use(logger.logRequest) // Mount route modules app.use('/auth', authRoutes) -app.use('/leaderboard', leaderboardRoutes) -app.use('/race-settings', raceSettingsRoutes) +app.use('/api/leaderboard', leaderboardRoutes) +app.use('/api/race-settings', raceSettingsRoutes) -// Initialize database and start server -initializeDatabase().then(() => { - app.listen(PORT, () => {}) +// Global error handler +app.use((error, req, res, _next) => { + logger.logError(error, { + context: 'Unhandled application error', + url: req.url, + method: req.method, + ip: req.ip, + userAgent: req.get('User-Agent'), + }) + + // Don't expose internal error details in production + const message = process.env.NODE_ENV === 'production' ? 'Internal server error' : error.message + + res.status(500).json({ error: message }) }) + +// Initialize database and start server +initializeDatabase() + .then(() => { + app.listen(PORT, () => { + logger.info('Server started successfully', { + port: PORT, + environment: process.env.NODE_ENV || 'development', + }) + }) + }) + .catch((error) => { + logger.logError(error, { context: 'Server startup failed' }) + process.exit(1) + }) diff --git a/src/routes/auth.js b/src/routes/auth.js index baac937..85a5c9c 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -1,5 +1,6 @@ const express = require('express') const { passport } = require('../config/auth') +const logger = require('../utils/logger') const router = express.Router() @@ -9,17 +10,21 @@ router.get('/google', passport.authenticate('google', { scope: ['profile', 'emai router.get( '/google/callback', passport.authenticate('google', { failureRedirect: '/' }), - (_req, res) => { + (req, res) => { // Successful authentication + logger.logAuth('google_callback_success', req.user) res.redirect('/') } ) router.post('/logout', (req, res) => { + const user = req.user req.logout((err) => { if (err) { + logger.logError(err, { context: 'Logout failed', user: user?.email }) return res.status(500).json({ error: 'Error logging out' }) } + logger.logAuth('logout', user, true) res.json({ message: 'Logged out successfully' }) }) }) diff --git a/src/routes/leaderboard.js b/src/routes/leaderboard.js index bfb660f..797ec85 100644 --- a/src/routes/leaderboard.js +++ b/src/routes/leaderboard.js @@ -7,17 +7,20 @@ const { getPublicFilePath, } = require('../utils/fileManager') const Leaderboard = require('../models/leaderboard') +const logger = require('../utils/logger') const router = express.Router() // Public leaderboard route (no auth required) router.get('/', async (_req, res) => { try { + logger.logDatabase('fetch', 'leaderboard') const leaderboards = await Leaderboard.findAll({ order: [['points', 'DESC']], }) res.json(leaderboards) - } catch (_error) { + } catch (error) { + logger.logError(error, { context: 'Failed to fetch leaderboards' }) res.status(500).json({ error: 'Error fetching leaderboards' }) } }) @@ -26,9 +29,15 @@ router.get('/', async (_req, res) => { router.post('/', isAuthorized, async (req, res) => { try { const { driverName, points } = req.body + logger.logDatabase('create', 'leaderboard', { driverName, points }) const newEntry = await Leaderboard.create({ driverName, points }) + logger.info('New leaderboard entry created', { id: newEntry.id, driverName }) res.status(201).json(newEntry) - } catch (_error) { + } catch (error) { + logger.logError(error, { + context: 'Failed to create leaderboard entry', + driverName: req.body.driverName, + }) res.status(500).json({ error: 'Error creating leaderboard entry' }) } }) @@ -43,6 +52,7 @@ router.post( const { id } = req.params if (!req.file) { + logger.warn('Profile picture upload failed: no file provided', { driverId: id }) return res.status(400).json({ error: 'No file uploaded' }) } @@ -50,12 +60,16 @@ router.post( if (!entry) { // Clean up uploaded file if driver not found cleanupUploadedFile(req) + logger.warn('Profile picture upload failed: driver not found', { driverId: id }) return res.status(404).json({ error: 'Leaderboard entry not found' }) } // Delete old profile picture if exists if (entry.profilePicture) { const oldPicturePath = getPublicFilePath(entry.profilePicture) + logger.logFileOperation('delete', entry.profilePicture, true, { + reason: 'replacing profile picture', + }) deleteFileIfExists(oldPicturePath) } @@ -63,13 +77,24 @@ router.post( const profilePicturePath = `/uploads/${req.file.filename}` await entry.update({ profilePicture: profilePicturePath }) + logger.logFileOperation('upload', req.file.filename, true, { + driverId: id, + driverName: entry.driverName, + fileSize: req.file.size, + }) + res.json({ message: 'Profile picture uploaded successfully', profilePicture: profilePicturePath, }) - } catch (_error) { + } catch (error) { // Clean up uploaded file on error cleanupUploadedFile(req) + logger.logError(error, { + context: 'Profile picture upload failed', + driverId: req.params.id, + filename: req.file?.filename, + }) res.status(500).json({ error: 'Error uploading profile picture' }) } } diff --git a/src/utils/appConfig.js b/src/utils/appConfig.js index 324adb2..85bdef6 100644 --- a/src/utils/appConfig.js +++ b/src/utils/appConfig.js @@ -11,7 +11,7 @@ function configureApp() { // Middleware app.use(express.json()) - app.use(express.static(path.join(__dirname, '../public'))) + app.use(express.static(path.join(__dirname, '../../public'))) // Session configuration app.use( diff --git a/src/utils/database.js b/src/utils/database.js index d78b247..2126e44 100644 --- a/src/utils/database.js +++ b/src/utils/database.js @@ -1,13 +1,21 @@ const sequelize = require('../config/db') +const logger = require('./logger') // Test database connection and sync models async function initializeDatabase() { try { + logger.info('Initializing database connection...') await sequelize.authenticate() + logger.info('Database connection established successfully') // Sync database models - force update to add new profilePicture column + logger.debug('Syncing database models...') await sequelize.sync({ alter: true }) - } catch (_error) {} + logger.info('Database models synchronized successfully') + } catch (error) { + logger.logError(error, { context: 'Database initialization failed' }) + throw error + } } // Check if race has expired (midnight PST on race day) diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..0ad7690 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,170 @@ +const winston = require('winston') +const path = require('node:path') + +// Define custom log levels and colors +const logLevels = { + fatal: 0, + error: 1, + warn: 2, + info: 3, + debug: 4, + trace: 5, +} + +const logColors = { + fatal: 'red', + error: 'red', + warn: 'yellow', + info: 'green', + debug: 'blue', + trace: 'magenta', +} + +// Add custom colors to winston +winston.addColors(logColors) + +// Create logs directory if it doesn't exist +const fs = require('node:fs') +const logsDir = path.join(process.cwd(), 'logs') +if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }) +} + +// Define log format +const logFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.printf(({ timestamp, level, message, stack, ...meta }) => { + let log = `${timestamp} [${level.toUpperCase()}]: ${message}` + + // Add stack trace for errors + if (stack) { + log += `\n${stack}` + } + + // Add additional metadata if present + const metaString = Object.keys(meta).length ? `\n${JSON.stringify(meta, null, 2)}` : '' + + return log + metaString + }) +) + +// Create the logger instance +const logger = winston.createLogger({ + levels: logLevels, + level: process.env.LOG_LEVEL || 'info', + format: logFormat, + transports: [ + // Write all logs to console + new winston.transports.Console({ + format: winston.format.combine(winston.format.colorize({ all: true }), logFormat), + }), + + // Write all logs to combined.log with daily rotation + new winston.transports.File({ + filename: path.join(logsDir, 'combined.log'), + maxsize: 10485760, // 10MB + maxFiles: 14, // Keep 2 weeks of logs + tailable: true, + zippedArchive: true, + }), + + // Write error logs to error.log with daily rotation + new winston.transports.File({ + filename: path.join(logsDir, 'error.log'), + level: 'error', + maxsize: 5242880, // 5MB + maxFiles: 30, // Keep 1 month of error logs + tailable: true, + zippedArchive: true, + }), + + // Write debug logs to debug.log with smaller retention + new winston.transports.File({ + filename: path.join(logsDir, 'debug.log'), + level: 'debug', + maxsize: 5242880, // 5MB + maxFiles: 7, // Keep 1 week of debug logs + tailable: true, + zippedArchive: true, + }), + ], + + // Handle uncaught exceptions and unhandled rejections with rotation + exceptionHandlers: [ + new winston.transports.File({ + filename: path.join(logsDir, 'exceptions.log'), + maxsize: 5242880, // 5MB + maxFiles: 10, + tailable: true, + }), + ], + + rejectionHandlers: [ + new winston.transports.File({ + filename: path.join(logsDir, 'rejections.log'), + maxsize: 5242880, // 5MB + maxFiles: 10, + tailable: true, + }), + ], +}) + +// Create helper functions for common logging patterns +logger.logRequest = (req, res, next) => { + const start = Date.now() + + res.on('finish', () => { + const duration = Date.now() - start + const { method, url, originalUrl, ip } = req + const { statusCode } = res + + const level = statusCode >= 400 ? 'warn' : 'info' + logger.log(level, `${method} ${originalUrl || url}`, { + statusCode, + duration: `${duration}ms`, + ip, + userAgent: req.get('User-Agent'), + forwardedFor: req.get('X-Forwarded-For'), + realIP: req.get('X-Real-IP'), + }) + }) + + if (next) next() +} + +logger.logError = (error, context = {}) => { + logger.error('Application error occurred', { + error: error.message, + stack: error.stack, + ...context, + }) +} + +logger.logAuth = (action, user, success = true, details = {}) => { + const level = success ? 'info' : 'warn' + logger.log(level, `Authentication ${action}`, { + user: user?.email || user?.id || 'unknown', + success, + ...details, + }) +} + +logger.logDatabase = (operation, table, details = {}) => { + logger.debug(`Database ${operation} on ${table}`, details) +} + +logger.logFileOperation = (operation, filename, success = true, details = {}) => { + const level = success ? 'info' : 'error' + logger.log(level, `File ${operation}: ${filename}`, { + success, + ...details, + }) +} + +// In development, also log to debug level +if (process.env.NODE_ENV === 'development') { + logger.level = 'debug' +} + +module.exports = logger From 33a2e1f25c2201e1d4b12c6358e5232e27cea553 Mon Sep 17 00:00:00 2001 From: Gio Coia Date: Mon, 11 Aug 2025 15:56:12 -0700 Subject: [PATCH 2/3] fix workflow --- .github/workflows/integration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 0b0ac3d..c895142 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -77,7 +77,7 @@ jobs: docker compose exec -T app curl -f http://localhost:3001/ -o /dev/null -s # Test internal application endpoints - docker compose exec -T app curl -f http://localhost:3001/race-settings -o /dev/null -s + docker compose exec -T app curl -f http://localhost:3001/api/race-settings -o /dev/null -s echo "✅ Application health test passed" @@ -89,7 +89,7 @@ jobs: curl -f http://localhost/ -o /dev/null -s -v # Test API endpoints through Caddy - curl -f http://localhost/race-settings -o /dev/null -s + curl -f http://localhost/api/race-settings -o /dev/null -s # Test that we get proper headers from Caddy response_headers=$(curl -I http://localhost/ 2>/dev/null) @@ -118,7 +118,7 @@ jobs: } # Test API endpoints return valid JSON - race_settings=$(curl -s http://localhost/race-settings) + race_settings=$(curl -s http://localhost/api/race-settings) echo "$race_settings" | jq . > /dev/null || { echo "❌ Race settings endpoint doesn't return valid JSON" echo "Response:" @@ -127,7 +127,7 @@ jobs: } # Test leaderboard endpoint - leaderboard=$(curl -s http://localhost/leaderboard) + leaderboard=$(curl -s http://localhost/api/leaderboard) echo "$leaderboard" | jq . > /dev/null || { echo "❌ Leaderboard endpoint doesn't return valid JSON" echo "Response:" From 24f544845304873fbcc68b4aaa17764e69082853 Mon Sep 17 00:00:00 2001 From: Giovanni Coia Date: Mon, 11 Aug 2025 15:56:55 -0700 Subject: [PATCH 3/3] Update src/utils/logger.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/utils/logger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/logger.js b/src/utils/logger.js index 0ad7690..a35c441 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -162,8 +162,8 @@ logger.logFileOperation = (operation, filename, success = true, details = {}) => }) } -// In development, also log to debug level -if (process.env.NODE_ENV === 'development') { +// In development, set log level to debug only if LOG_LEVEL is not explicitly set +if (process.env.NODE_ENV === 'development' && !process.env.LOG_LEVEL) { logger.level = 'debug' }