Skip to content

Commit

Permalink
Fix esbuild-related issues
Browse files Browse the repository at this point in the history
* 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 <signed@mruder.dev>
  • Loading branch information
marvinruder committed Aug 11, 2023
1 parent 0ed142c commit bbdf59a
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 132 deletions.
25 changes: 0 additions & 25 deletions .pnp-ts.loader.mjs

This file was deleted.

21 changes: 17 additions & 4 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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" ]
15 changes: 8 additions & 7 deletions docker/Dockerfile-build
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
"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",
"test:tools": "conc --kill-others \"yarn workspace @rating-tracker/backend test:tools\" -n \"\" -p \"{name}\" -c grey --timings",
"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": {
Expand All @@ -32,5 +32,10 @@
},
"resolutions": {
"vite/esbuild": "0.19.0"
},
"dependenciesMeta": {
"swagger-ui-dist@5.3.1": {
"unplugged": true
}
}
}
4 changes: 3 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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": {
Expand All @@ -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",
Expand Down
14 changes: 13 additions & 1 deletion packages/backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
154 changes: 68 additions & 86 deletions packages/backend/src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -54,37 +49,22 @@ 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();

/**
* 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(
Expand All @@ -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("");
Expand All @@ -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
Expand All @@ -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;
Loading

0 comments on commit bbdf59a

Please sign in to comment.