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:
committed by
GitHub
parent
63f8b8e805
commit
585fa873cb
@@ -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
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user