diff --git a/packages/frontend-2/lib/core/helpers/redis.ts b/packages/frontend-2/lib/core/helpers/redis.ts new file mode 100644 index 0000000000..56e3ff69b1 --- /dev/null +++ b/packages/frontend-2/lib/core/helpers/redis.ts @@ -0,0 +1,28 @@ +import { Redis } from 'ioredis' +import type pino from 'pino' + +export const createRedis = async (params: { logger: pino.Logger }) => { + const { logger } = params + const { redisUrl } = useRuntimeConfig() + if (!redisUrl?.length) { + return undefined + } + + const redis = new Redis(redisUrl) + + redis.on('error', (err) => { + logger.error(err, 'Redis error') + }) + + redis.on('end', () => { + logger.info('Redis disconnected from server') + }) + + // Try to ping the server + const res = await redis.ping() + if (res !== 'PONG') { + throw new Error('Redis server did not respond to ping') + } + + return redis +} diff --git a/packages/frontend-2/package.json b/packages/frontend-2/package.json index 626652fc0e..7b3756074b 100644 --- a/packages/frontend-2/package.json +++ b/packages/frontend-2/package.json @@ -120,7 +120,7 @@ "tailwindcss": "^3.4.1", "type-fest": "^3.5.1", "typescript": "^4.8.3", - "vue-tsc": "1.8.22", + "vue-tsc": "1.8.27", "wait-on": "^6.0.1" }, "engines": { diff --git a/packages/frontend-2/plugins/002-rum.ts b/packages/frontend-2/plugins/002-rum.ts index 5051a4724d..358fe31b50 100644 --- a/packages/frontend-2/plugins/002-rum.ts +++ b/packages/frontend-2/plugins/002-rum.ts @@ -5,7 +5,7 @@ import { useCreateErrorLoggingTransport } from '~/lib/core/composables/error' type PluginNuxtApp = Parameters[0] async function initRumClient(app: PluginNuxtApp) { - const { enabled, keys, speckleServerVersion } = resolveInitParams() + const { enabled, keys, speckleServerVersion, baseUrl } = resolveInitParams() const logger = useLogger() const onAuthStateChange = useOnAuthStateChange() const router = useRouter() @@ -20,6 +20,7 @@ async function initRumClient(app: PluginNuxtApp) { rg4js('enablePulse', true) rg4js('boot') rg4js('enableRum', true) + rg4js('withTags', [`baseUrl:${baseUrl}`, `version:${speckleServerVersion}`]) await onAuthStateChange( (user, { resolveDistinctId }) => { @@ -184,7 +185,8 @@ function resolveInitParams() { logrocketAppId, speckleServerVersion, speedcurveId, - debugbearId + debugbearId, + baseUrl } } = useRuntimeConfig() const raygun = raygunKey?.length ? raygunKey : null @@ -201,7 +203,8 @@ function resolveInitParams() { speedcurve, debugbear }, - speckleServerVersion + speckleServerVersion, + baseUrl } } diff --git a/packages/frontend-2/plugins/004-redis.server.ts b/packages/frontend-2/plugins/004-redis.server.ts index ecb34b4107..a8ab0e18f6 100644 --- a/packages/frontend-2/plugins/004-redis.server.ts +++ b/packages/frontend-2/plugins/004-redis.server.ts @@ -1,4 +1,5 @@ import { Redis } from 'ioredis' +import { createRedis } from '~/lib/core/helpers/redis' /** * Re-using the same client for all SSR reqs (shouldn't be a problem) @@ -9,31 +10,20 @@ let redis: InstanceType | undefined = undefined * Provide redis (only in SSR) */ export default defineNuxtPlugin(async () => { - const { redisUrl } = useRuntimeConfig() const logger = useLogger() - if (redisUrl?.length) { - try { - const hasValidStatus = - redis && ['ready', 'connecting', 'reconnecting'].includes(redis.status) - if (!redis || !hasValidStatus) { - if (redis) { - await redis.quit() - } - - redis = new Redis(redisUrl) - - redis.on('error', (err) => { - logger.error(err, 'Redis error') - }) - - redis.on('end', () => { - logger.info('Redis disconnected from server') - }) + try { + const hasValidStatus = + redis && ['ready', 'connecting', 'reconnecting'].includes(redis.status) + if (!redis || !hasValidStatus) { + if (redis) { + await redis.quit() } - } catch (e) { - logger.error(e, 'Redis setup failure') + + redis = await createRedis({ logger }) } + } catch (e) { + logger.error(e, 'Redis setup failure') } const isValid = redis && redis.status === 'ready' diff --git a/packages/frontend-2/server/api/status.ts b/packages/frontend-2/server/api/status.ts index 4420e70026..58543ec1cf 100644 --- a/packages/frontend-2/server/api/status.ts +++ b/packages/frontend-2/server/api/status.ts @@ -1,6 +1,28 @@ -import { useRequestId } from '~/lib/core/composables/server' +import { ensureError } from '@speckle/shared' +import { createRedis } from '~/lib/core/helpers/redis' -export default defineEventHandler((event) => { - const reqId = useRequestId({ event }) - return { status: 'ok', reqId } +/** + * Check that the deployment is fine + */ + +export default defineEventHandler(async () => { + let redisConnected = false + + // Check that redis works + try { + const redis = await createRedis({ logger: useLogger() }) + redisConnected = !!redis + if (redis) { + await redis.quit() + } + } catch (e) { + const errMsg = ensureError(e).message + throw createError({ + statusCode: 500, + fatal: true, + message: `Redis connection failed: ${errMsg}` + }) + } + + return { status: 'ok', redisConnected } }) diff --git a/packages/frontend-2/server/lib/core/helpers/observability.ts b/packages/frontend-2/server/lib/core/helpers/observability.ts index a2cf1cf832..82e0dc7180 100644 --- a/packages/frontend-2/server/lib/core/helpers/observability.ts +++ b/packages/frontend-2/server/lib/core/helpers/observability.ts @@ -3,6 +3,7 @@ import { Observability } from '@speckle/shared' import type { IncomingMessage } from 'node:http' import { get } from 'lodash-es' import type { Logger } from 'pino' +import type express from 'express' const redactedReqHeaders = ['authorization', 'cookie'] @@ -44,7 +45,7 @@ export function serializeRequest(req: IncomingMessage) { return { id: req.id, method: req.method, - path: req.url?.split('?')[0], // Remove query params which might be sensitive + path: getRequestPath(req), // Allowlist useful headers headers: Object.keys(req.headers).reduce((obj, key) => { let valueToPrint = req.headers[key] @@ -58,3 +59,10 @@ export function serializeRequest(req: IncomingMessage) { }, {}) } } + +export const getRequestPath = (req: IncomingMessage | express.Request) => { + const path = ((get(req, 'originalUrl') || get(req, 'url') || '') as string).split( + '?' + )[0] as string + return path?.length ? path : null +} diff --git a/packages/frontend-2/server/middleware/001-logging.ts b/packages/frontend-2/server/middleware/001-logging.ts index 732c8e7799..5e2895178d 100644 --- a/packages/frontend-2/server/middleware/001-logging.ts +++ b/packages/frontend-2/server/middleware/001-logging.ts @@ -1,4 +1,3 @@ -import { Observability } from '@speckle/shared' import { defineEventHandler, fromNodeMiddleware } from 'h3' import { IncomingMessage, ServerResponse } from 'http' import pino from 'pino' @@ -9,7 +8,10 @@ import { randomUUID } from 'crypto' import type { IncomingHttpHeaders } from 'http' import { REQUEST_ID_HEADER } from '~~/server/lib/core/helpers/constants' import { get } from 'lodash' -import { serializeRequest } from '~/server/lib/core/helpers/observability' +import { + serializeRequest, + getRequestPath +} from '~/server/lib/core/helpers/observability' /** * Server request logger @@ -28,10 +30,7 @@ function determineRequestId( const generateReqId: GenReqId = (req: IncomingMessage) => determineRequestId(req.headers) -const logger = Observability.getLogger( - useRuntimeConfig().public.logLevel, - useRuntimeConfig().public.logPretty -) +const logger = useLogger() export const LoggingMiddleware = pinoHttp({ logger, @@ -46,8 +45,9 @@ export const LoggingMiddleware = pinoHttp({ error: Error | undefined ) => { // Mark some lower importance/spammy endpoints w/ 'debug' to reduce noise - const path = req.url?.split('?')[0] - const shouldBeDebug = ['/metrics', '/health'].includes(path || '') ?? false + const path = getRequestPath(req) + const shouldBeDebug = + ['/metrics', '/health', '/api/status'].includes(path || '') ?? false if (res.statusCode >= 400 && res.statusCode < 500) { return 'info' @@ -66,7 +66,7 @@ export const LoggingMiddleware = pinoHttp({ customSuccessObject(req, res, val: Record) { const isCompleted = !req.readableAborted && res.writableEnded const requestStatus = isCompleted ? 'completed' : 'aborted' - const requestPath = req.url?.split('?')[0] || 'unknown' + const requestPath = getRequestPath(req) || 'unknown' const appBindings = res.vueLoggerBindings || {} return { @@ -82,7 +82,7 @@ export const LoggingMiddleware = pinoHttp({ }, customErrorObject(req, res, err, val: Record) { const requestStatus = 'failed' - const requestPath = req.url?.split('?')[0] || 'unknown' + const requestPath = getRequestPath(req) || 'unknown' const appBindings = res.vueLoggerBindings || {} return { @@ -107,9 +107,10 @@ export const LoggingMiddleware = pinoHttp({ const realRaw = get(res, 'raw.raw') as typeof res.raw const isRequestCompleted = !!realRaw.writableEnded const isRequestAborted = !isRequestCompleted + const statusCode = res.statusCode || res.raw.statusCode || realRaw.statusCode return { - statusCode: res.raw.statusCode, + statusCode, // Allowlist useful headers headers: resRaw.headers, isRequestAborted diff --git a/packages/frontend-2/server/tsconfig.json b/packages/frontend-2/server/tsconfig.json new file mode 100644 index 0000000000..35676ea1b4 --- /dev/null +++ b/packages/frontend-2/server/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../.nuxt/tsconfig.server.json", + "compilerOptions": { + "verbatimModuleSyntax": true + } +} diff --git a/packages/frontend-2/server/utils/logger.ts b/packages/frontend-2/server/utils/logger.ts new file mode 100644 index 0000000000..754390b3b1 --- /dev/null +++ b/packages/frontend-2/server/utils/logger.ts @@ -0,0 +1,29 @@ +import type { Optional } from '@speckle/shared' +import type pino from 'pino' +import { buildLogger } from '~/server/lib/core/helpers/observability' + +let logger: Optional = undefined + +const createLogger = () => { + const { + public: { logLevel, logPretty, speckleServerVersion, serverName } + } = useRuntimeConfig() + + const logger = buildLogger(logLevel, logPretty).child({ + browser: false, + speckleServerVersion, + serverName, + frontendType: 'frontend-2', + serverLogger: true + }) + + return logger +} + +export const useLogger = () => { + if (!logger) { + logger = createLogger() + } + + return logger +} diff --git a/packages/server/logging/expressLogging.ts b/packages/server/logging/expressLogging.ts index 1211d1af21..c731da064c 100644 --- a/packages/server/logging/expressLogging.ts +++ b/packages/server/logging/expressLogging.ts @@ -106,9 +106,10 @@ export const LoggingExpressMiddleware = HttpLogger({ } const serverRes = get(res, 'raw.raw') as ServerResponse const auth = serverRes.req.context + const statusCode = res.statusCode || res.raw.statusCode || serverRes.statusCode return { - statusCode: res.raw.statusCode, + statusCode, // Allowlist useful headers headers: Object.fromEntries( Object.entries(resRaw.raw.headers).filter( diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml index 36c73a5358..b7209638fb 100644 --- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml +++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml @@ -44,7 +44,7 @@ spec: livenessProbe: httpGet: - path: /health + path: /api/status port: www failureThreshold: 3 initialDelaySeconds: 10 @@ -53,7 +53,7 @@ spec: timeoutSeconds: 5 readinessProbe: httpGet: - path: /health + path: /api/status port: www failureThreshold: 1 initialDelaySeconds: 5 diff --git a/utils/helm/speckle-server/templates/redirect.ingress.yml b/utils/helm/speckle-server/templates/redirect.ingress.yml index a185d9c0b4..0c0fdbbe2d 100644 --- a/utils/helm/speckle-server/templates/redirect.ingress.yml +++ b/utils/helm/speckle-server/templates/redirect.ingress.yml @@ -26,4 +26,17 @@ spec: name: speckle-frontend port: name: www +{{- end }} + - pathType: Exact + path: "/api/status" + backend: + service: +{{- if .Values.frontend_2.enabled }} + name: speckle-frontend-2 + port: + name: web +{{- else }} + name: speckle-frontend + port: + name: www {{- end }} diff --git a/yarn.lock b/yarn.lock index 0def78d774..734be864d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13902,7 +13902,7 @@ __metadata: vee-validate: ^4.7.0 vue-advanced-cropper: ^2.8.8 vue-tippy: ^6.0.0 - vue-tsc: 1.8.22 + vue-tsc: 1.8.27 wait-on: ^6.0.1 ws: ^8.9.0 languageName: unknown @@ -18241,6 +18241,15 @@ __metadata: languageName: node linkType: hard +"@volar/language-core@npm:1.11.1, @volar/language-core@npm:~1.11.1": + version: 1.11.1 + resolution: "@volar/language-core@npm:1.11.1" + dependencies: + "@volar/source-map": 1.11.1 + checksum: 7f98fbeb96ff1093dbaa47e790575a98d1fd2103d9bb1598ec7b0ae787fc6af2ffcea12fdea0f0a4e057f38f6ee3a60bd54f2af3985159319021771f79df9451 + languageName: node + linkType: hard + "@volar/language-core@npm:1.4.0-alpha.4": version: 1.4.0-alpha.4 resolution: "@volar/language-core@npm:1.4.0-alpha.4" @@ -18268,6 +18277,15 @@ __metadata: languageName: node linkType: hard +"@volar/source-map@npm:1.11.1, @volar/source-map@npm:~1.11.1": + version: 1.11.1 + resolution: "@volar/source-map@npm:1.11.1" + dependencies: + muggle-string: ^0.3.1 + checksum: 1ec1034432ee51a0afe187ba9158292dd607a90d01120ee8a36cf27f5d464da5282c8fe7b0de82f52f45474a840c63eba666254c5c21ca5466dc02d0c95cd147 + languageName: node + linkType: hard + "@volar/source-map@npm:1.4.0-alpha.4": version: 1.4.0-alpha.4 resolution: "@volar/source-map@npm:1.4.0-alpha.4" @@ -18305,6 +18323,16 @@ __metadata: languageName: node linkType: hard +"@volar/typescript@npm:~1.11.1": + version: 1.11.1 + resolution: "@volar/typescript@npm:1.11.1" + dependencies: + "@volar/language-core": 1.11.1 + path-browserify: ^1.0.1 + checksum: 0db2fc32db133e493f05dbafd248560a6d4e5b071a0d80422c67b1875bd36980c113915d876a83e855d55c2880b2e7b9f04f803ce3504a4d6fafcc0b801c621b + languageName: node + linkType: hard + "@volar/vue-language-core@npm:1.3.4": version: 1.3.4 resolution: "@volar/vue-language-core@npm:1.3.4" @@ -19105,6 +19133,28 @@ __metadata: languageName: node linkType: hard +"@vue/language-core@npm:1.8.27": + version: 1.8.27 + resolution: "@vue/language-core@npm:1.8.27" + dependencies: + "@volar/language-core": ~1.11.1 + "@volar/source-map": ~1.11.1 + "@vue/compiler-dom": ^3.3.0 + "@vue/shared": ^3.3.0 + computeds: ^0.0.1 + minimatch: ^9.0.3 + muggle-string: ^0.3.1 + path-browserify: ^1.0.1 + vue-template-compiler: ^2.7.14 + peerDependencies: + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 8660c05319be8dc5daacc2cd929171434215d29f3ad5bfbe0038d1967db05b8bf640286b25f338845cc1e3890b4aaa239ac9e8cb832cc8a50a5bbdff31b2edd1 + languageName: node + linkType: hard + "@vue/language-core@npm:1.8.8": version: 1.8.8 resolution: "@vue/language-core@npm:1.8.8" @@ -46632,7 +46682,22 @@ __metadata: languageName: node linkType: hard -"vue-tsc@npm:1.8.22, vue-tsc@npm:^1.8.20, vue-tsc@npm:^1.8.22": +"vue-tsc@npm:1.8.27": + version: 1.8.27 + resolution: "vue-tsc@npm:1.8.27" + dependencies: + "@volar/typescript": ~1.11.1 + "@vue/language-core": 1.8.27 + semver: ^7.5.4 + peerDependencies: + typescript: "*" + bin: + vue-tsc: bin/vue-tsc.js + checksum: 98c2986df01000a3245b5f08b9db35d0ead4f46fb12f4fe771257b4aa61aa4c26dda359aaa0e6c484a6240563d5188aaa6ed312dd37cc2315922d5e079260001 + languageName: node + linkType: hard + +"vue-tsc@npm:^1.8.20, vue-tsc@npm:^1.8.22": version: 1.8.22 resolution: "vue-tsc@npm:1.8.22" dependencies: