fix(fe-2): structured logging fixes
This commit is contained in:
@@ -3,13 +3,41 @@ import { Optional } from '@speckle/shared'
|
||||
import { buildFakePinoLogger } from '~~/lib/core/helpers/observability'
|
||||
|
||||
export const useLogger = () => {
|
||||
let nuxtApp: Optional<NuxtApp>
|
||||
return useNuxtApp().$logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Use when you need to be sure that the real structured pino logger is available
|
||||
* (it isn't in some early startup contexts like apollo link setup)
|
||||
*/
|
||||
export const useStrictLogger = async (
|
||||
options?: Partial<{ dontNotifyFallback: boolean }>
|
||||
) => {
|
||||
const { dontNotifyFallback } = options || {}
|
||||
|
||||
let nuxtApp: Optional<NuxtApp> = undefined
|
||||
try {
|
||||
nuxtApp = useNuxtApp()
|
||||
} catch (e) {
|
||||
console.error('Nuxt app for logger not found! Falling back to fake logger...')
|
||||
return buildFakePinoLogger()
|
||||
// suppress 'nuxt is not available'
|
||||
}
|
||||
|
||||
return nuxtApp?.$logger
|
||||
if (nuxtApp?.$logger) return nuxtApp?.$logger
|
||||
|
||||
// Nuxt app not found in this scope
|
||||
const err = new Error(
|
||||
'Nuxt app for logger not found! Initializing fallback structured logger...'
|
||||
)
|
||||
|
||||
let logger: ReturnType<typeof buildFakePinoLogger>
|
||||
if (process.server) {
|
||||
const { buildLogger } = await import('~/server/lib/core/helpers/observability')
|
||||
logger = buildLogger('info', process.dev ? true : false) // no runtime config, so falling back to default settings
|
||||
} else {
|
||||
logger = buildFakePinoLogger()
|
||||
}
|
||||
|
||||
if (!dontNotifyFallback) logger.error(err)
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
@@ -369,6 +369,7 @@ export type Commit = {
|
||||
authorAvatar?: Maybe<Scalars['String']>;
|
||||
authorId?: Maybe<Scalars['String']>;
|
||||
authorName?: Maybe<Scalars['String']>;
|
||||
branch?: Maybe<Branch>;
|
||||
branchName?: Maybe<Scalars['String']>;
|
||||
/**
|
||||
* The total number of comments for this commit. To actually get the comments, use the comments query and pass in a resource array consisting of of this commit's id.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { isObjectLike as lodashIsObjectLike } from 'lodash-es'
|
||||
import { SetNonNullable, SetRequired } from 'type-fest'
|
||||
|
||||
export type NonUndefined<T> = T extends undefined ? never : T
|
||||
@@ -15,3 +16,6 @@ export type AddParameters<
|
||||
TFunction extends (...args: any) => any,
|
||||
TParameters extends [...args: any]
|
||||
> = (...args: [...Parameters<TFunction>, ...TParameters]) => ReturnType<TFunction>
|
||||
|
||||
export const isObjectLike = (value: unknown): value is Record<string, unknown> =>
|
||||
lodashIsObjectLike(value)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createUploadLink } from 'apollo-upload-client'
|
||||
import { WebSocketLink } from '@apollo/client/link/ws'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
import { OperationDefinitionNode, Kind } from 'graphql'
|
||||
import { CookieRef } from '#app'
|
||||
import { CookieRef, NuxtApp } from '#app'
|
||||
import { Optional } from '@speckle/shared'
|
||||
import { useAuthCookie } from '~~/lib/auth/composables/auth'
|
||||
import {
|
||||
@@ -20,6 +20,7 @@ import { onError } from '@apollo/client/link/error'
|
||||
import { useNavigateToLogin, loginRoute } from '~~/lib/common/helpers/route'
|
||||
import { useAppErrorState } from '~~/lib/core/composables/appErrorState'
|
||||
import { isInvalidAuth } from '~~/lib/common/helpers/graphql'
|
||||
import { omit } from 'lodash-es'
|
||||
|
||||
const appName = 'frontend-2'
|
||||
|
||||
@@ -263,19 +264,28 @@ function createLink(params: {
|
||||
httpEndpoint: string
|
||||
wsClient?: SubscriptionClient
|
||||
authToken: CookieRef<Optional<string>>
|
||||
nuxtApp: NuxtApp
|
||||
}): ApolloLink {
|
||||
const { httpEndpoint, wsClient, authToken } = params
|
||||
const { httpEndpoint, wsClient, authToken, nuxtApp } = params
|
||||
const goToLogin = useNavigateToLogin()
|
||||
const { registerError, isErrorState } = useAppErrorState()
|
||||
|
||||
const errorLink = onError((res) => {
|
||||
const logger = useLogger()
|
||||
|
||||
const logger = nuxtApp.$logger
|
||||
const isSubTokenMissingError = (res.networkError?.message || '').includes(
|
||||
'need a token to subscribe'
|
||||
)
|
||||
|
||||
if (!isSubTokenMissingError) logger.error('Apollo Client error', res)
|
||||
if (!isSubTokenMissingError) {
|
||||
logger.error(
|
||||
{
|
||||
...omit(res, ['forward']),
|
||||
networkErrorMessage: res.networkError?.message,
|
||||
gqlErrorMessages: res.graphQLErrors?.map((e) => e.message)
|
||||
},
|
||||
'Apollo Client error'
|
||||
)
|
||||
}
|
||||
|
||||
const { networkError } = res
|
||||
if (networkError && isInvalidAuth(networkError)) {
|
||||
@@ -348,6 +358,7 @@ const defaultConfigResolver: ApolloConfigResolver = async () => {
|
||||
const {
|
||||
public: { apiOrigin, speckleServerVersion = 'unknown' }
|
||||
} = useRuntimeConfig()
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
const httpEndpoint = `${apiOrigin}/graphql`
|
||||
const wsEndpoint = httpEndpoint.replace('http', 'ws')
|
||||
@@ -356,7 +367,7 @@ const defaultConfigResolver: ApolloConfigResolver = async () => {
|
||||
const wsClient = process.client
|
||||
? await createWsClient({ wsEndpoint, authToken })
|
||||
: undefined
|
||||
const link = createLink({ httpEndpoint, wsClient, authToken })
|
||||
const link = createLink({ httpEndpoint, wsClient, authToken, nuxtApp })
|
||||
|
||||
return {
|
||||
// If we don't markRaw the cache, sometimes we get cryptic internal Apollo Client errors that essentially
|
||||
|
||||
@@ -5,13 +5,21 @@
|
||||
import { Observability } from '@speckle/shared'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
export function buildFakePinoLogger() {
|
||||
export function buildFakePinoLogger(
|
||||
options?: Partial<{ onError: (...args: any[]) => void }>
|
||||
) {
|
||||
const errLogger = (...args: unknown[]) => {
|
||||
const { onError } = options || {}
|
||||
if (onError) onError(...args)
|
||||
console.error(...args)
|
||||
}
|
||||
|
||||
const logger = {
|
||||
debug: console.debug,
|
||||
info: console.info,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
fatal: console.error,
|
||||
error: errLogger,
|
||||
fatal: errLogger,
|
||||
trace: console.debug,
|
||||
silent: noop
|
||||
} as unknown as ReturnType<typeof Observability.getLogger>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { isObjectLike } from '~~/lib/common/helpers/type'
|
||||
import { buildFakePinoLogger } from '~~/lib/core/helpers/observability'
|
||||
|
||||
/**
|
||||
@@ -18,38 +20,60 @@ export default defineNuxtPlugin(async () => {
|
||||
|
||||
let logger: ReturnType<typeof import('@speckle/shared').Observability.getLogger>
|
||||
if (process.server) {
|
||||
const { Observability } = await import('@speckle/shared')
|
||||
logger = Observability.getLogger(logLevel, logPretty)
|
||||
const { buildLogger } = await import('~/server/lib/core/helpers/observability')
|
||||
logger = buildLogger(logLevel, logPretty)
|
||||
} else {
|
||||
logger = buildFakePinoLogger()
|
||||
|
||||
// set up seq ingestion
|
||||
if (!process.dev && logClientApiToken?.length && logClientApiEndpoint?.length) {
|
||||
if (logClientApiToken?.length && logClientApiEndpoint?.length) {
|
||||
const seq = await import('seq-logging/browser')
|
||||
const logger = new seq.Logger({
|
||||
const seqLogger = new seq.Logger({
|
||||
serverUrl: logClientApiEndpoint,
|
||||
apiKey: logClientApiToken,
|
||||
onError: console.error
|
||||
})
|
||||
|
||||
const errorListener = (event: ErrorEvent) => {
|
||||
logger.emit({
|
||||
const errorListener = (
|
||||
event: ErrorEvent | PromiseRejectionEvent | string | Error | unknown
|
||||
) => {
|
||||
const isUnhandledRejection = isObjectLike(event) && 'reason' in event
|
||||
let err: Error
|
||||
if (event instanceof Error) {
|
||||
err = event
|
||||
} else if (isObjectLike(event)) {
|
||||
if ('reason' in event && event.reason instanceof Error) {
|
||||
err = event.reason
|
||||
} else if ('error' in event && event.error instanceof Error) {
|
||||
err = event.error
|
||||
} else {
|
||||
err = new Error(`${JSON.stringify(event)}`)
|
||||
}
|
||||
} else {
|
||||
err = new Error(`${event}`)
|
||||
}
|
||||
|
||||
seqLogger.emit({
|
||||
timestamp: new Date(),
|
||||
level: 'error',
|
||||
messageTemplate: 'Client-side error: {errorMessage}',
|
||||
properties: {
|
||||
errorMessage: event.message,
|
||||
errorMessage: err.message,
|
||||
browser: true,
|
||||
frontendType: 'frontend-2',
|
||||
speckleServerVersion,
|
||||
serverName
|
||||
serverName,
|
||||
isUnhandledRejection
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
exception: event.error instanceof Error ? event.error.stack : `${event.error}`
|
||||
exception: err.stack
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('error', errorListener)
|
||||
window.addEventListener('unhandledrejection', errorListener)
|
||||
|
||||
logger = buildFakePinoLogger({ onError: errorListener })
|
||||
logger.debug('Set up seq ingestion...')
|
||||
} else {
|
||||
logger = buildFakePinoLogger()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Observability } from '@speckle/shared'
|
||||
|
||||
export function buildLogger(logLevel: string, logPretty: boolean) {
|
||||
return Observability.getLogger(logLevel, logPretty)
|
||||
}
|
||||
Reference in New Issue
Block a user