feat(fe2): proper health probe endpoint - /api/status - [WBX-287] (#2086)

* feat: proper health probe endpoint - /api/status

* preventing external access to status endpoint

* linting fix
This commit is contained in:
Kristaps Fabians Geikins
2024-02-27 16:34:33 +02:00
committed by GitHub
parent 63f8b8e805
commit 585fa873cb
13 changed files with 209 additions and 46 deletions
@@ -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
}
+1 -1
View File
@@ -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": {
+6 -3
View File
@@ -5,7 +5,7 @@ import { useCreateErrorLoggingTransport } from '~/lib/core/composables/error'
type PluginNuxtApp = Parameters<Plugin>[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
}
}
+11 -21
View File
@@ -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<typeof Redis> | 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'
+23 -4
View File
@@ -1,6 +1,25 @@
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
} catch (e) {
const errMsg = ensureError(e).message
throw createError({
statusCode: 500,
fatal: true,
message: `Redis connection failed: ${errMsg}`
})
}
return { status: 'ok', redisConnected }
})
@@ -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
}
@@ -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<string, unknown>) {
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<string, unknown>) {
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
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../.nuxt/tsconfig.server.json",
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
@@ -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<pino.Logger> = 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
}
+2 -1
View File
@@ -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(
@@ -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
@@ -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 }}
+67 -2
View File
@@ -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: