From 93d540fb6ab1a4b2f861f4f017b54b6ca95526a0 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Wed, 21 Aug 2024 09:18:15 +0000 Subject: [PATCH] fix: logging setup and error card error display (#349) --- nodemon.json | 3 ++- package-lock.json | 42 +++++++++++++++++++++++++++++++++++++++-- package.json | 1 + src/index.ts | 10 +++++----- src/pkg/logger.ts | 30 +++++++++++++++++++++++++++-- src/routes/500.ts | 48 +++++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 120 insertions(+), 14 deletions(-) diff --git a/nodemon.json b/nodemon.json index 589ae322..bb1695e7 100644 --- a/nodemon.json +++ b/nodemon.json @@ -9,6 +9,7 @@ "DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES": "true", "ORY_SDK_URL": "http://localhost:4433", "KRATOS_PUBLIC_URL": "http://localhost:4433", - "KRATOS_ADMIN_URL": "http://localhost:4434" + "KRATOS_ADMIN_URL": "http://localhost:4434", + "NODE_ENV": "development" } } diff --git a/package-lock.json b/package-lock.json index ef789dc0..bfb29013 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@ory/kratos-selfservice-ui-node", - "version": "0.18.0", + "version": "0.19.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ory/kratos-selfservice-ui-node", - "version": "0.18.0", + "version": "0.19.0", "license": "Apache-2.0", "dependencies": { "@ory/client": "1.14.3", "@ory/elements-markup": "0.3.0-rc.1", + "@redtea/format-axios-error": "2.1.1", "accept-language-parser": "1.5.0", "axios": "1.7.4", "body-parser": "1.20.2", @@ -680,6 +681,28 @@ "node": ">= 8" } }, + "node_modules/@redtea/format-axios-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@redtea/format-axios-error/-/format-axios-error-2.1.1.tgz", + "integrity": "sha512-bGfqYctnX/+nBuHsOo6RDP8JrHoetnf6BEK5+v1lmbVg5w9kvt/UmKg+5YlT3YCj4UQZX86LdN6cZF083A9fyA==", + "license": "ISC", + "dependencies": { + "logform": "2.2.0" + } + }, + "node_modules/@redtea/format-axios-error/node_modules/logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "license": "MIT", + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -1388,6 +1411,15 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -2040,6 +2072,12 @@ "node": ">=8.6.0" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", diff --git a/package.json b/package.json index 585a5fb2..269ff3fb 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "dependencies": { "@ory/client": "1.14.3", "@ory/elements-markup": "0.3.0-rc.1", + "@redtea/format-axios-error": "2.1.1", "accept-language-parser": "1.5.0", "axios": "1.7.4", "body-parser": "1.20.2", diff --git a/src/index.ts b/src/index.ts index 00d4275f..a529256d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,7 +116,7 @@ app.use(baseUrl, router) const port = Number(process.env.PORT) || 3000 let listener = (proto: "http" | "https") => () => { - console.log(`Listening on ${proto}://0.0.0.0:${port}`) + logger.info(`Listening on ${proto}://0.0.0.0:${port}`) } // When using the Ory Admin API Token, we assume that this application is also @@ -128,16 +128,16 @@ if ( String(process.env.CSRF_COOKIE_NAME || "").length === 0 || String(process.env.CSRF_COOKIE_SECRET || "").length < 8 ) { - console.error( + logger.error( "Cannot start the server without the required environment variables!", ) - console.error( + logger.error( "COOKIE_SECRET must be set and be at least 8 alphanumerical character `export COOKIE_SECRET=...`", ) - console.error( + logger.error( "CSRF_COOKIE_NAME must be set! Prefix the name to scope it to your domain `__HOST-` `export CSRF_COOKIE_NAME=...`", ) - console.error( + logger.error( "CSRF_COOKIE_SECRET must be set and be at least 8 alphanumerical character `export CSRF_COOKIE_SECRET=...`", ) } else { diff --git a/src/pkg/logger.ts b/src/pkg/logger.ts index 71dc68bb..c6c8beab 100644 --- a/src/pkg/logger.ts +++ b/src/pkg/logger.ts @@ -3,9 +3,35 @@ import expressWinston from "express-winston" import winston from "winston" +let format: winston.Logform.Format = winston.format.json() +if (process.env.NODE_ENV === "development") { + format = winston.format.combine( + winston.format.simple(), + winston.format.colorize({ + all: true, + colors: { + info: "blue", + error: "red", + warn: "yellow", + }, + }), + ) +} + const config = { - format: winston.format.json(), + format: winston.format.combine(winston.format.timestamp(), format), transports: [new winston.transports.Console()], } export const logger = winston.createLogger(config) -export const middleware = expressWinston.logger({ winstonInstance: logger }) +export const middleware = expressWinston.logger({ + winstonInstance: logger, + ignoreRoute: (req) => req.url.startsWith("/assets"), + ignoredRoutes: [ + "/theme.css", + "/main.css", + "/content-layout.css", + "/style.css", + "/ory-small.svg", + "/favico.png", + ], +}) diff --git a/src/routes/500.ts b/src/routes/500.ts index 05daadf9..f21cc4ae 100644 --- a/src/routes/500.ts +++ b/src/routes/500.ts @@ -1,21 +1,61 @@ // Copyright © 2022 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { RouteRegistrator } from "../pkg" +import { logger, RouteRegistrator } from "../pkg" import { UserErrorCard } from "@ory/elements-markup" +import { format } from "@redtea/format-axios-error" import { NextFunction, Request, Response } from "express" +function formatRequest(req: Request) { + return { + headers: req.headers, + method: req.method, + url: req.url, + httpVersion: req.httpVersion, + body: req.body, + cookies: req.cookies, + path: req.path, + protocol: req.protocol, + query: req.query, + hostname: req.hostname, + ip: req.ip, + originalUrl: req.originalUrl, + params: req.params, + } +} + export const register500Route: RouteRegistrator = (app, createHelpers) => { app.use((err: Error, req: Request, res: Response, next: NextFunction) => { - console.error(err, err.stack) + let jsonError: string = "" + try { + const formattedError = format(err) + + logger.error("An error occurred", { + error: formattedError, + req: formatRequest(req), + }) + delete (formattedError as any).config + delete (formattedError as any).isAxiosError + jsonError = JSON.stringify(formattedError) + } catch (e) { + // This shouldn't happen, as the try block should not throw an error. + // But if it does, we log it and render the Error card with a generic error message. + // If this is removed, the server will crash if the error is not serializable. + logger.error("An error occurred while serializing the error", { + error: err, + req: formatRequest(req), + }) + } res.status(500).render("error", { card: UserErrorCard({ title: "Internal Server Error", cardImage: createHelpers?.(req, res).logoUrl, backUrl: req.header("Referer") || "welcome", error: { - id: "backend-error", + id: "interal_server_error", error: { - ...err, + message: + "An internal server error occurred. Please try again later.", + debug: JSON.parse(jsonError), }, }, }),