Skip to content

Commit

Permalink
feat: fe2 status check endpoint w/ proper redis cleanup (#2092)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabis94 authored Feb 28, 2024
1 parent df3c138 commit edb1c63
Show file tree
Hide file tree
Showing 13 changed files with 212 additions and 46 deletions.
28 changes: 28 additions & 0 deletions packages/frontend-2/lib/core/helpers/redis.ts
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion packages/frontend-2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
9 changes: 6 additions & 3 deletions packages/frontend-2/plugins/002-rum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -184,7 +185,8 @@ function resolveInitParams() {
logrocketAppId,
speckleServerVersion,
speedcurveId,
debugbearId
debugbearId,
baseUrl
}
} = useRuntimeConfig()
const raygun = raygunKey?.length ? raygunKey : null
Expand All @@ -201,7 +203,8 @@ function resolveInitParams() {
speedcurve,
debugbear
},
speckleServerVersion
speckleServerVersion,
baseUrl
}
}

Expand Down
32 changes: 11 additions & 21 deletions packages/frontend-2/plugins/004-redis.server.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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'
Expand Down
30 changes: 26 additions & 4 deletions packages/frontend-2/server/api/status.ts
Original file line number Diff line number Diff line change
@@ -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 }
})
10 changes: 9 additions & 1 deletion packages/frontend-2/server/lib/core/helpers/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down Expand Up @@ -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]
Expand All @@ -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
}
23 changes: 12 additions & 11 deletions packages/frontend-2/server/middleware/001-logging.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Observability } from '@speckle/shared'
import { defineEventHandler, fromNodeMiddleware } from 'h3'
import { IncomingMessage, ServerResponse } from 'http'
import pino from 'pino'
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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'
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/frontend-2/server/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../.nuxt/tsconfig.server.json",
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
29 changes: 29 additions & 0 deletions packages/frontend-2/server/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion packages/server/logging/expressLogging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions utils/helm/speckle-server/templates/frontend_2/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ spec:

livenessProbe:
httpGet:
path: /health
path: /api/status
port: www
failureThreshold: 3
initialDelaySeconds: 10
Expand All @@ -53,7 +53,7 @@ spec:
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health
path: /api/status
port: www
failureThreshold: 1
initialDelaySeconds: 5
Expand Down
13 changes: 13 additions & 0 deletions utils/helm/speckle-server/templates/redirect.ingress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Loading

0 comments on commit edb1c63

Please sign in to comment.