From bbdf59a4f7ac0776479f095859810c23f672456c Mon Sep 17 00:00:00 2001 From: "Marvin A. Ruder" Date: Fri, 11 Aug 2023 19:58:55 +0200 Subject: [PATCH] Fix esbuild-related issues * Fix Swagger UI is not available #369 * Fix Error class names are wrong in logfile #370 * Improve logging and health check Signed-off-by: Marvin A. Ruder --- .pnp-ts.loader.mjs | 25 ----- .pnp.cjs | 21 +++- docker/Dockerfile | 2 +- docker/Dockerfile-build | 15 +-- package.json | 9 +- packages/backend/package.json | 4 +- packages/backend/src/server.ts | 14 ++- packages/backend/src/utils/logger.ts | 154 ++++++++++++--------------- packages/commons/package.json | 2 +- yarn.lock | 23 +++- 10 files changed, 137 insertions(+), 132 deletions(-) delete mode 100644 .pnp-ts.loader.mjs diff --git a/.pnp-ts.loader.mjs b/.pnp-ts.loader.mjs deleted file mode 100644 index 4c80a4289..000000000 --- a/.pnp-ts.loader.mjs +++ /dev/null @@ -1,25 +0,0 @@ -import Path from "node:path"; -import URL from "node:url"; -import FS from "node:fs"; - -export function resolve(specifier, context, next) { - if (!specifier.startsWith(".") || !specifier.endsWith(".js")) { - return next(specifier, context); - } - - const parentURL = context.parentURL; - if (!parentURL || !parentURL.startsWith("file:") || parentURL.includes("/.yarn/")) { - return next(specifier, context); - } - - const dirName = Path.dirname(URL.fileURLToPath(parentURL)); - const baseName = specifier.slice(0, -3); - const path = Path.join(dirName, baseName); - for (const extension of [".js", ".ts", ".tsx"]) { - if (FS.existsSync(path + extension)) { - return next(baseName + extension, context); - } - } - - return next(specifier, context); -} diff --git a/.pnp.cjs b/.pnp.cjs index d35502f3d..cadf5e476 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -1641,6 +1641,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@types/node", "npm:18.17.4"],\ ["@types/selenium-webdriver", "npm:4.1.15"],\ ["@types/supertest", "npm:2.0.12"],\ + ["@types/swagger-ui-express", "npm:4.1.3"],\ ["@typescript-eslint/eslint-plugin", "virtual:9e2d75c26d812ba07f2548643e31c2f0eb2cb6f6eca268f33f7e7f2f00bc9a60e5174f4187df59beb0c43929d43a06842c0155865f0f7f541c96499f2ed6aada#npm:6.3.0"],\ ["@typescript-eslint/parser", "virtual:9e2d75c26d812ba07f2548643e31c2f0eb2cb6f6eca268f33f7e7f2f00bc9a60e5174f4187df59beb0c43929d43a06842c0155865f0f7f541c96499f2ed6aada#npm:6.3.0"],\ ["@vitest/coverage-v8", "virtual:9e2d75c26d812ba07f2548643e31c2f0eb2cb6f6eca268f33f7e7f2f00bc9a60e5174f4187df59beb0c43929d43a06842c0155865f0f7f541c96499f2ed6aada#npm:0.34.1"],\ @@ -1672,6 +1673,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["response-time", "npm:2.3.2"],\ ["selenium-webdriver", "npm:4.11.1"],\ ["supertest", "npm:6.3.3"],\ + ["swagger-ui-dist", "npm:5.3.1"],\ ["swagger-ui-express", "virtual:143f48b3b02030f94479b3a5988283313dc9859fa4cb6844b22a91f8f01b9f50780f19a990f97707ad409e89183b3679a8db4d101263b8caf9410c781cd5e728#npm:5.0.0"],\ ["typescript", "patch:typescript@npm%3A5.1.6#~builtin::version=5.1.6&hash=5da071"],\ ["vite", "virtual:143f48b3b02030f94479b3a5988283313dc9859fa4cb6844b22a91f8f01b9f50780f19a990f97707ad409e89183b3679a8db4d101263b8caf9410c781cd5e728#npm:4.4.9"],\ @@ -2534,6 +2536,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@types/swagger-ui-express", [\ + ["npm:4.1.3", {\ + "packageLocation": "./.yarn/cache/@types-swagger-ui-express-npm-4.1.3-0c91a9cfb5-1c990fa8c1.zip/node_modules/@types/swagger-ui-express/",\ + "packageDependencies": [\ + ["@types/swagger-ui-express", "npm:4.1.3"],\ + ["@types/express", "npm:4.17.17"],\ + ["@types/serve-static", "npm:1.15.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/treeify", [\ ["npm:1.0.0", {\ "packageLocation": "./.yarn/cache/@types-treeify-npm-1.0.0-b5e04e9cd3-1b2397030d.zip/node_modules/@types/treeify/",\ @@ -9677,10 +9690,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["swagger-ui-dist", [\ - ["npm:5.1.0", {\ - "packageLocation": "./.yarn/cache/swagger-ui-dist-npm-5.1.0-2571f16bc3-41b91708e7.zip/node_modules/swagger-ui-dist/",\ + ["npm:5.3.1", {\ + "packageLocation": "./.yarn/unplugged/swagger-ui-dist-npm-5.3.1-af568da2af/node_modules/swagger-ui-dist/",\ "packageDependencies": [\ - ["swagger-ui-dist", "npm:5.1.0"]\ + ["swagger-ui-dist", "npm:5.3.1"]\ ],\ "linkType": "HARD"\ }]\ @@ -9699,7 +9712,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["swagger-ui-express", "virtual:143f48b3b02030f94479b3a5988283313dc9859fa4cb6844b22a91f8f01b9f50780f19a990f97707ad409e89183b3679a8db4d101263b8caf9410c781cd5e728#npm:5.0.0"],\ ["@types/express", "npm:4.17.17"],\ ["express", "npm:4.18.2"],\ - ["swagger-ui-dist", "npm:5.1.0"]\ + ["swagger-ui-dist", "npm:5.3.1"]\ ],\ "packagePeers": [\ "@types/express",\ diff --git a/docker/Dockerfile b/docker/Dockerfile index 66185d123..b32ea70ae 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -48,6 +48,6 @@ USER node COPY --from=assemble --chown=node:node /workdir/app . # Define health check -HEALTHCHECK --interval=5m --start-period=15s CMD wget -qO /dev/null http://localhost:$PORT/api/status || exit 1 +HEALTHCHECK CMD wget -qO /dev/null http://localhost:$PORT/api/status || exit 1 CMD [ "dumb-init", "node", "server.mjs" ] diff --git a/docker/Dockerfile-build b/docker/Dockerfile-build index 9419ccda8..2a911e525 100644 --- a/docker/Dockerfile-build +++ b/docker/Dockerfile-build @@ -5,15 +5,16 @@ ENV FORCE_COLOR true WORKDIR /workdir -# Copy project files and WebAssembly package -COPY . . +# Copy project files as well as Swagger UI files and WebAssembly package COPY --from=marvinruder/rating-tracker:wasm /workdir/pkg packages/wasm +COPY .yarn/unplugged/swagger-ui-dist-*/node_modules/swagger-ui-dist/swagger-ui.css .yarn/unplugged/swagger-ui-dist-*/node_modules/swagger-ui-dist/swagger-ui-bundle.js .yarn/unplugged/swagger-ui-dist-*/node_modules/swagger-ui-dist/swagger-ui-standalone-preset.js /workdir/app/packages/backend/dist/public/api-docs/ +COPY . . -# Build Node.js bundle -RUN yarn build - -# Create directories for target container and copy only necessary files -RUN mkdir -p app/public app/prisma/client && \ +RUN \ + # Build Node.js bundle + yarn build && \ + # Create directories for target container and copy only necessary files + mkdir -p app/public app/prisma/client && \ cp packages/backend/dist/server.mjs app && \ cp -r packages/backend/prisma/client/schema.prisma packages/backend/prisma/client/libquery_engine-* app/prisma/client && \ cp -r packages/frontend/dist/* app/public diff --git a/package.json b/package.json index 642151493..477c6122d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "scripts": { "dev:server": "conc --kill-others \"yarn workspace @rating-tracker/backend dev:run\" \"yarn workspace @rating-tracker/backend dev:watch\" \"yarn workspace @rating-tracker/frontend dev:vite\" \"yarn workspace @rating-tracker/commons dev:build\" -n \",,,ﯤ\" -p \"{name}\" -c #339933,#3178C6,#61DAFB,#3178C6 --timings", "dev:tools": "conc --kill-others \"yarn workspace @rating-tracker/backend dev:tools\" -n \"\" -p \"{name}\" -c grey --timings", - "dev:wasm": "wasm-pack build -s rating-tracker -d ../packages/wasm --debug wasm", "prisma:migrate:dev": "yarn workspace @rating-tracker/backend prisma:migrate:dev", "prisma:studio": "yarn workspace @rating-tracker/backend prisma:studio", "test": "yarn workspaces foreach -pt run test", @@ -21,7 +20,8 @@ "test:prisma:migrate:init": "yarn workspace @rating-tracker/backend test:prisma:migrate:init", "build": "yarn workspaces foreach -pt run build", "build:wasm": "wasm-pack build -s rating-tracker -d ../packages/wasm --release wasm && sed -E -i.bak 's/\"module\": \"([A-Za-z0-9\\-\\.]+)\",/\"main\": \"\\1\",\\\n \"module\": \"\\1\",/g ; s/^}$/}\\\n/' packages/wasm/package.json && rm packages/wasm/package.json.bak", - "lint": "yarn workspaces foreach -pt run lint" + "lint": "yarn workspaces foreach -pt run lint", + "fix:swagger": "mkdir -p packages/backend/dist/public/api-docs && cp .yarn/unplugged/swagger-ui-dist-*/node_modules/swagger-ui-dist/swagger-ui{.css,-bundle.js,-standalone-preset.js} packages/backend/dist/public/api-docs/" }, "packageManager": "yarn@3.6.1", "devDependencies": { @@ -32,5 +32,10 @@ }, "resolutions": { "vite/esbuild": "0.19.0" + }, + "dependenciesMeta": { + "swagger-ui-dist@5.3.1": { + "unplugged": true + } } } diff --git a/packages/backend/package.json b/packages/backend/package.json index ceb42d0da..005e33356 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -26,7 +26,7 @@ "prisma:studio": "pnpify prisma studio", "prisma:generate": "pnpify prisma generate", "prisma:migrate:dev": "pnpify prisma migrate dev", - "build": "esbuild --color=true src/server.ts --bundle --minify --platform=node --format=esm --legal-comments=none --banner:js=\"import r from'path';import{createRequire as e}from'module';import{fileURLToPath as m}from'url';const require=e(import.meta.url),__filename=m(import.meta.url),__dirname=r.dirname(__filename);\" --outfile=dist/server.mjs", + "build": "esbuild --color=true src/server.ts --bundle --minify --keep-names --platform=node --format=esm --legal-comments=none --banner:js=\"import __path from'path';import{createRequire}from'module';import{fileURLToPath}from'url';const require=createRequire(import.meta.url),__filename=fileURLToPath(import.meta.url),__dirname=__path.dirname(__filename);\" --outfile=dist/server.mjs", "lint": "eslint --cache --ext .ts src/", "lint:fix": "eslint --cache --ext .ts src/ --fix" }, @@ -50,6 +50,7 @@ "redis-om": "0.4.2", "response-time": "2.3.2", "selenium-webdriver": "4.11.1", + "swagger-ui-dist": "5.3.1", "swagger-ui-express": "5.0.0" }, "devDependencies": { @@ -58,6 +59,7 @@ "@types/node": "18.17.4", "@types/selenium-webdriver": "4.1.15", "@types/supertest": "2.0.12", + "@types/swagger-ui-express": "4.1.3", "@typescript-eslint/eslint-plugin": "6.3.0", "@typescript-eslint/parser": "6.3.0", "@vitest/coverage-v8": "0.34.1", diff --git a/packages/backend/src/server.ts b/packages/backend/src/server.ts index fc0412ad4..5b2c6fa84 100644 --- a/packages/backend/src/server.ts +++ b/packages/backend/src/server.ts @@ -140,7 +140,19 @@ server.app.use((req, res, next) => { }); // Host the OpenAPI UI -server.app.use("/api-docs", SwaggerUI.serve, SwaggerUI.setup(openapiDocument)); +server.app.use( + "/api-docs", + SwaggerUI.serve, + SwaggerUI.setup( + openapiDocument, + undefined, + undefined, + undefined, + "/assets/images/favicon-dev/favicon-192.png", + undefined, + "Rating Tracker API", + ), +); // Host the OpenAPI JSON configuration server.app.get("/api-spec/v3", (_, res) => res.json(openapiDocument)); diff --git a/packages/backend/src/utils/logger.ts b/packages/backend/src/utils/logger.ts index 88dd33c59..0cd280382 100644 --- a/packages/backend/src/utils/logger.ts +++ b/packages/backend/src/utils/logger.ts @@ -30,19 +30,14 @@ const levelIcons = { /** * The stream used to log messages to the standard output. */ -const prettyStream = pretty({ - include: "level", - customPrettifiers: { - level: (level) => levelIcons[Number(level)], - }, -}); +const prettyStream = pretty({ include: "level", customPrettifiers: { level: (level) => levelIcons[Number(level)] } }); /** * Provides the path of the log file for the current day. * * @returns {string} The path of the log file. */ -const getLogFilePath = () => { +const getLogFilePath = (): string => { return (process.env.LOG_FILE ?? "/tmp/rating-tracker-log-(DATE).log").replaceAll( "(DATE)", new Date().toISOString().split("T")[0], @@ -54,11 +49,7 @@ const getLogFilePath = () => { * * @returns {fs.WriteStream} The stream to write to the log file. */ -const getNewFileStream = () => { - return fs.createWriteStream(getLogFilePath(), { - flags: "a", - }); -}; +const getNewFileStream = (): fs.WriteStream => fs.createWriteStream(getLogFilePath(), { flags: "a" }); let fileStream = getNewFileStream(); @@ -66,25 +57,14 @@ let fileStream = getNewFileStream(); * A multistream which writes to both the standard output and the log file. */ const multistream = pino.multistream([ - { - level: (process.env.LOG_LEVEL as pino.Level) ?? "info", - stream: prettyStream, - }, - { - level: (process.env.LOG_LEVEL as pino.Level) ?? "info", - stream: fileStream, - }, + { level: (process.env.LOG_LEVEL as pino.Level) ?? "info", stream: prettyStream }, + { level: (process.env.LOG_LEVEL as pino.Level) ?? "info", stream: fileStream }, ]); /** * The logger used to log messages to both the standard output and the log file. */ -const logger = pino( - { - level: process.env.LOG_LEVEL ?? "info", - }, - multistream, -); +const logger = pino({ level: process.env.LOG_LEVEL ?? "info" }, multistream); // Rotate the log file every day new cron.CronJob( @@ -104,7 +84,7 @@ new cron.CronJob( * @param {string} method The HTTP method. * @returns {string} A colored pretty prefix string. */ -const highlightMethod = (method: string) => { +const highlightMethod = (method: string): string => { switch (method) { case "GET": return chalk.whiteBright.bgBlue(` ${method} `) + chalk.blue.bgGrey(""); @@ -127,7 +107,7 @@ const highlightMethod = (method: string) => { * @param {number} statusCode The HTTP status code. * @returns {string} A colored pretty prefix string. */ -const statusCodeDescription = (statusCode: number) => { +const statusCodeDescription = (statusCode: number): string => { const statusCodeString = ` ${statusCode}  ${STATUS_CODES[statusCode]} `; switch (Math.floor(statusCode / 100)) { case 2: // Successful responses @@ -147,64 +127,66 @@ const statusCodeDescription = (statusCode: number) => { * @param {Request} req Request object * @param {Response} res Response object * @param {number} time The response time of the request. + * @returns {void} */ -export const requestLogger = (req: Request, res: Response, time: number) => { - // Do not log requests for resources such as logos – those are far too many and only mildly interesting - if (!req.originalUrl.startsWith(`/api${stockLogoEndpointPath}`)) { - chalk - .white( - chalk.whiteBright.bgHex("#339933")(" \uf898 ") + - chalk.bgGrey.hex("#339933")("") + - chalk.bgGrey( - chalk.cyanBright(" \uf5ef " + new Date().toISOString()) + // Timestamp - "  " + - chalk.yellow( - res.locals.user - ? `\uf007 ${res.locals.user.name} (${res.locals.user.email})` // Authenticated user - : /* c8 ignore next */ // We do not test Cron jobs - res.locals.userIsCron - ? "\ufba7 cron" // Cron job - : "\uf21b", // Unauthenticated user - ) + - "  " + - chalk.magentaBright("\uf98c" + req.ip) + // IP address - " ", - ) + - chalk.grey("") + - "\n ├─" + - highlightMethod(req.method) + // HTTP request method - chalk.bgGrey( - ` ${req.originalUrl // URL path - .slice(1, req.originalUrl.indexOf("?") == -1 ? undefined : req.originalUrl.indexOf("?")) - .replaceAll("/", "  ")} `, - ) + - chalk.grey("") + - Object.entries(req.cookies) // Cookies - .map( - ([key, value]) => - "\n ├─" + chalk.bgGrey(chalk.yellow(" \uf697") + `  ${key} `) + chalk.grey("") + " " + value, - ) - .join(" ") + - Object.entries(req.query) // Query parameters - .map( - ([key, value]) => - "\n ├─" + chalk.bgGrey(chalk.cyan(" \uf002") + `  ${key} `) + chalk.grey("") + " " + value, - ) - .join(" ") + - "\n ╰─" + - statusCodeDescription(res.statusCode) + // HTTP response status code - ` ${ - res.hasHeader("Content-Length") && res.hasHeader("Content-Type") - ? `sent ${res.getHeader("Content-Length")} bytes of type “${ - res.getHeader("Content-Type").toString().split(";")[0] - }” ` - : "" - }after ${Math.round(time)} ms`, // Response time - ) - .split("\n") - .forEach((line) => logger.info(line)); // Show newlines in the log in a pretty way - logger.info(""); - } -}; +export const requestLogger = (req: Request, res: Response, time: number): void => + chalk + .white( + chalk.whiteBright.bgHex("#339933")(" \uf898 ") + + chalk.bgGrey.hex("#339933")("") + + chalk.bgGrey( + chalk.cyanBright(" \uf5ef " + new Date().toISOString()) + // Timestamp + "  " + + chalk.yellow( + res.locals.user + ? `\uf007 ${res.locals.user.name} (${res.locals.user.email})` // Authenticated user + : /* c8 ignore next */ // We do not test Cron jobs + res.locals.userIsCron + ? "\ufba7 cron" // Cron job + : "\uf21b", // Unauthenticated user + ) + + "  " + + chalk.magentaBright("\uf98c" + req.ip) + // IP address + " ", + ) + + chalk.grey("") + + "\n ├─" + + highlightMethod(req.method) + // HTTP request method + chalk.bgGrey( + ` ${req.originalUrl // URL path + .slice(1, req.originalUrl.indexOf("?") == -1 ? undefined : req.originalUrl.indexOf("?")) + .replaceAll("/", "  ")} `, + ) + + chalk.grey("") + + Object.entries(req.cookies) // Cookies + .map( + ([key, value]) => + "\n ├─" + chalk.bgGrey(chalk.yellow(" \uf697") + `  ${key} `) + chalk.grey("") + " " + value, + ) + .join(" ") + + Object.entries(req.query) // Query parameters + .map( + ([key, value]) => + "\n ├─" + chalk.bgGrey(chalk.cyan(" \uf002") + `  ${key} `) + chalk.grey("") + " " + value, + ) + .join(" ") + + "\n ╰─" + + statusCodeDescription(res.statusCode) + // HTTP response status code + ` ${ + res.hasHeader("Content-Length") && res.hasHeader("Content-Type") + ? `sent ${res.getHeader("Content-Length")} bytes of type “${ + res.getHeader("Content-Type").toString().split(";")[0] + }” ` + : "" + }after ${Math.round(time)} ms +`, // Response time + ) + .split("\n") + // Log internal requests or requests for resources such as logos as debug messages – those are far too many and + // typically only mildly interesting + .forEach((line) => + // Show newlines in the log in a pretty way + logger[req.originalUrl.startsWith(`/api${stockLogoEndpointPath}`) || req.ip === "::1" ? "trace" : "info"](line), + ); export default logger; diff --git a/packages/commons/package.json b/packages/commons/package.json index 5e9a5a4c7..253c3f9c8 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -12,7 +12,7 @@ "types": "./src/index.ts", "scripts": { "dev:build": "yarn build --watch", - "build": "esbuild --color=true src/index.ts --bundle --minify --platform=node --format=esm --legal-comments=none --outfile=dist/index.js", + "build": "esbuild --color=true src/index.ts --bundle --minify --keep-names --platform=node --format=esm --legal-comments=none --outfile=dist/index.js", "test:watch": "vitest", "test": "vitest run --color", "lint": "eslint --cache --ext .ts src/", diff --git a/yarn.lock b/yarn.lock index f3ecdf724..58f05a74c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1216,6 +1216,7 @@ __metadata: "@types/node": 18.17.4 "@types/selenium-webdriver": 4.1.15 "@types/supertest": 2.0.12 + "@types/swagger-ui-express": 4.1.3 "@typescript-eslint/eslint-plugin": 6.3.0 "@typescript-eslint/parser": 6.3.0 "@vitest/coverage-v8": 0.34.1 @@ -1247,6 +1248,7 @@ __metadata: response-time: 2.3.2 selenium-webdriver: 4.11.1 supertest: 6.3.3 + swagger-ui-dist: 5.3.1 swagger-ui-express: 5.0.0 typescript: 5.1.6 vite: 4.4.9 @@ -1334,6 +1336,9 @@ __metadata: eslint: 8.47.0 prettier: 3.0.1 typescript: 5.1.6 + dependenciesMeta: + swagger-ui-dist@5.3.1: + unplugged: true languageName: unknown linkType: soft @@ -1948,6 +1953,16 @@ __metadata: languageName: node linkType: hard +"@types/swagger-ui-express@npm:4.1.3": + version: 4.1.3 + resolution: "@types/swagger-ui-express@npm:4.1.3" + dependencies: + "@types/express": "*" + "@types/serve-static": "*" + checksum: 1c990fa8c158f699c5443245383daef2c4b47efce715c105e67f9b88447cf23663774393fdeea7d5a6e97a83d6959b09129f19c4abca64abdb7db74517162295 + languageName: node + linkType: hard + "@types/treeify@npm:^1.0.0": version: 1.0.0 resolution: "@types/treeify@npm:1.0.0" @@ -8148,10 +8163,10 @@ __metadata: languageName: node linkType: hard -"swagger-ui-dist@npm:>=5.0.0": - version: 5.1.0 - resolution: "swagger-ui-dist@npm:5.1.0" - checksum: 41b91708e757852423a4fddfc07d0e87ef38c4ad9fad5757fbc27b23c9e71593a1c48b53661fa2d9bb241043b6800cd9a57d980943a17c9eb388704e30156120 +"swagger-ui-dist@npm:5.3.1, swagger-ui-dist@npm:>=5.0.0": + version: 5.3.1 + resolution: "swagger-ui-dist@npm:5.3.1" + checksum: babf26f3a40a932db22248a620dbb0c5c68721c4165b173537a949c6ec3df3a89982db484651e1f5fe31f3cdb1037eeef13fb6b746efbb72902d7410291070c8 languageName: node linkType: hard