feat(fe2): safe logger composable for logging anywhere (#5288)
* feat(fe2): safe logger composable for logging anywhere * devlogger adjusted too * allow suppressing fallback logger usage * report fallback only once
This commit is contained in:
committed by
GitHub
parent
8c745ad853
commit
4d67ac41b7
@@ -153,7 +153,7 @@ import type { ProjectFragment } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
- **Structured logging** with Pino for production
|
||||
- **useLogger()** composable for standard logging
|
||||
- **useStrictLogger()** when you need guaranteed real Pino logger
|
||||
- **useSafeLogger()** when you need a logger potentially outside of useNuxtApp() scope
|
||||
- **devLog()** or **useDevLogger()** for development-only logging
|
||||
- **Never use console.log** - use logging composables instead
|
||||
- **Development logging** is automatically skipped in production
|
||||
|
||||
@@ -9,71 +9,95 @@ export const useLogger = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* There are scopes where useNuxtApp() is available and we have to fall back to a different logger. This composable
|
||||
* can be invoked everywhere in all scopes (not in `server` code tho!) to get the best kind of logger available
|
||||
*/
|
||||
export const useStrictLogger = async (
|
||||
options?: Partial<{ dontNotifyFallback: boolean }>
|
||||
export const useSafeLogger = (
|
||||
options?: Partial<{
|
||||
/**
|
||||
* Prevent reporting back that we used a logger fallback.
|
||||
*
|
||||
* Default: false
|
||||
*/
|
||||
preventFallbackReporting: boolean
|
||||
}>
|
||||
) => {
|
||||
const { dontNotifyFallback } = options || {}
|
||||
|
||||
let fallbackReported = false
|
||||
let nuxtApp: Optional<NuxtApp> = undefined
|
||||
try {
|
||||
nuxtApp = useNuxtApp()
|
||||
} catch {
|
||||
// suppress 'nuxt is not available'
|
||||
const fallbackSyncLogger = buildFakePinoLogger()
|
||||
let fallbackFullLogger: Optional<ReturnType<typeof buildFakePinoLogger>> = undefined
|
||||
|
||||
const availableLogger = () =>
|
||||
nuxtApp?.$logger || fallbackFullLogger || fallbackSyncLogger
|
||||
|
||||
const tryResolvingRealLogger = () => {
|
||||
// Try nuxt app
|
||||
try {
|
||||
nuxtApp = useNuxtApp()
|
||||
} catch {
|
||||
// suppress 'nuxt is not available'
|
||||
}
|
||||
}
|
||||
|
||||
if (nuxtApp?.$logger) return nuxtApp?.$logger
|
||||
const tryResolvingLogger = async () => {
|
||||
try {
|
||||
tryResolvingRealLogger()
|
||||
if (nuxtApp?.$logger) return
|
||||
|
||||
// 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 (import.meta.server) {
|
||||
const { buildLogger } = await import('~/server/lib/core/helpers/observability')
|
||||
logger = buildLogger('info', import.meta.dev ? true : false) // no runtime config, so falling back to default settings
|
||||
} else {
|
||||
logger = buildFakePinoLogger()
|
||||
if (import.meta.server && !fallbackFullLogger) {
|
||||
const { buildLogger } = await import('~/server/lib/core/helpers/observability')
|
||||
fallbackFullLogger = buildLogger('info', import.meta.dev ? true : false) // no runtime config, so falling back to default settings
|
||||
}
|
||||
} catch (err) {
|
||||
availableLogger().error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (!dontNotifyFallback) logger.error(err)
|
||||
const logger = (
|
||||
loggerOptions?: Partial<{
|
||||
/**
|
||||
* Prevent reporting back that we used a logger fallback.
|
||||
*
|
||||
* Default: false
|
||||
*/
|
||||
preventFallbackReporting: boolean
|
||||
}>
|
||||
) => {
|
||||
const preventFallbackReporting =
|
||||
loggerOptions?.preventFallbackReporting || options?.preventFallbackReporting
|
||||
|
||||
return logger
|
||||
}
|
||||
void tryResolvingLogger()
|
||||
if (nuxtApp?.$logger) return nuxtApp.$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)
|
||||
*
|
||||
* The async version is better in that it will build a real pino logger, but in sync contexts
|
||||
* you can use this one that will at least fallback to console.log/warn/error
|
||||
*/
|
||||
export const useStrictLoggerSync = (
|
||||
options?: Partial<{ dontNotifyFallback: boolean }>
|
||||
) => {
|
||||
const { dontNotifyFallback } = options || {}
|
||||
const err = new Error('Nuxt app for logger not found! Returning fallback logger...')
|
||||
let logger: ReturnType<typeof buildFakePinoLogger>
|
||||
if (fallbackFullLogger) {
|
||||
logger = fallbackFullLogger
|
||||
} else {
|
||||
logger = fallbackSyncLogger
|
||||
}
|
||||
|
||||
let nuxtApp: Optional<NuxtApp> = undefined
|
||||
try {
|
||||
nuxtApp = useNuxtApp()
|
||||
} catch {
|
||||
// suppress 'nuxt is not available'
|
||||
// we only wanna report this once
|
||||
if (!preventFallbackReporting && !fallbackReported) {
|
||||
logger.error(err)
|
||||
fallbackReported = true
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
if (nuxtApp?.$logger) return nuxtApp?.$logger
|
||||
void tryResolvingLogger()
|
||||
|
||||
// Nuxt app not found in this scope
|
||||
const err = new Error(
|
||||
'Nuxt app for logger not found! Initializing fallback structured logger...'
|
||||
)
|
||||
const logger = buildFakePinoLogger()
|
||||
|
||||
if (!dontNotifyFallback) logger.error(err)
|
||||
|
||||
return logger
|
||||
return {
|
||||
/**
|
||||
* Get the best available logger instance
|
||||
*/
|
||||
logger,
|
||||
/**
|
||||
* If you're in a scope where you can invoke async code, invoke this to try to load the most appropriate logger
|
||||
*/
|
||||
loadBestLogger: tryResolvingLogger
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +107,11 @@ export const useStrictLoggerSync = (
|
||||
export const useDevLogger = () => {
|
||||
if (!import.meta.dev) return noop
|
||||
|
||||
const logger = useLogger()
|
||||
const debug = logger.debug.bind(logger)
|
||||
return debug as (...args: unknown[]) => void
|
||||
const { logger } = useSafeLogger()
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (...args: any[]) => {
|
||||
const actualLogger = logger()
|
||||
const debug = actualLogger.debug.bind(actualLogger)
|
||||
return debug(args[0], ...args.slice(1)) //ts appeasement
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ const useResetAuthState = (
|
||||
const resolveDistinctId = useResolveUserDistinctId()
|
||||
const { cbs } = useOnAuthStateChangeState()
|
||||
const authToken = useAuthCookie()
|
||||
const logger = useStrictLoggerSync()
|
||||
const { logger } = useSafeLogger()
|
||||
|
||||
return async (
|
||||
resetOptions?: Partial<{
|
||||
@@ -174,7 +174,7 @@ const useResetAuthState = (
|
||||
// also depend on active user (e.g. Workspace.seatType))
|
||||
resetPromise = (async () => {
|
||||
if (import.meta.server) {
|
||||
logger?.error('attempting to resetStore from SSR')
|
||||
logger().error('attempting to resetStore from SSR')
|
||||
} else {
|
||||
await client.resetStore()
|
||||
}
|
||||
@@ -229,7 +229,7 @@ export const useAuthManager = (
|
||||
const getMixpanel = useDeferredMixpanel()
|
||||
const postAuthRedirect = usePostAuthRedirect()
|
||||
const { markLoggedOut } = useJustLoggedOutTracking()
|
||||
const logger = useStrictLoggerSync()
|
||||
const { logger } = useSafeLogger()
|
||||
|
||||
/**
|
||||
* Invite token, if any
|
||||
@@ -379,7 +379,7 @@ export const useAuthManager = (
|
||||
title: 'Authentication failed',
|
||||
description: err.message
|
||||
})
|
||||
logger.error({ err }, 'Failed to finalize login with access code')
|
||||
logger().error({ err }, 'Failed to finalize login with access code')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ export type { AsyncWritableComputedOptions, AsyncWritableComputedRef }
|
||||
* @param params
|
||||
*/
|
||||
export const writableAsyncComputed: typeof originalWritableAsyncComputed = (params) => {
|
||||
const logger = useStrictLoggerSync()
|
||||
const { logger } = useSafeLogger()
|
||||
return originalWritableAsyncComputed({
|
||||
...params,
|
||||
debugging: params.debugging?.log
|
||||
@@ -21,7 +21,7 @@ export const writableAsyncComputed: typeof originalWritableAsyncComputed = (para
|
||||
...params.debugging,
|
||||
log: {
|
||||
...params.debugging.log,
|
||||
logger: logger.debug
|
||||
logger: logger().debug
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
@@ -34,7 +34,7 @@ export const writableAsyncComputed: typeof originalWritableAsyncComputed = (para
|
||||
*/
|
||||
export const watchAsync = ((...args: Parameters<typeof watch>) => {
|
||||
const [source, cb, options] = args
|
||||
const logger = useStrictLoggerSync()
|
||||
const { logger } = useSafeLogger()
|
||||
|
||||
const watches = shallowRef<Array<Promise<unknown>>>([])
|
||||
|
||||
@@ -47,7 +47,7 @@ export const watchAsync = ((...args: Parameters<typeof watch>) => {
|
||||
const handlerPromise = Promise.allSettled(watches.value).finally(() =>
|
||||
Promise.resolve(cb(newVal, oldVal, onCleanup))
|
||||
.catch((e) => {
|
||||
logger.error(e, 'Error occurred in watchAsync callback')
|
||||
logger().error(e, 'Error occurred in watchAsync callback')
|
||||
throw e
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -11,19 +11,19 @@ export function wrapRefWithTracking<R extends Ref<unknown>>(
|
||||
): R {
|
||||
const { writesOnly, readsOnly } = options || {}
|
||||
const getTrace = () => (new Error('Trace:').stack || '').substring(7)
|
||||
const logger = useStrictLoggerSync()
|
||||
const { logger } = useSafeLogger()
|
||||
|
||||
return computed({
|
||||
get: () => {
|
||||
if (!writesOnly) {
|
||||
logger.debug(`debugging: '${name}' read`, ref.value, getTrace())
|
||||
logger().debug(`debugging: '${name}' read`, ref.value, getTrace())
|
||||
}
|
||||
|
||||
return ref.value
|
||||
},
|
||||
set: (newVal) => {
|
||||
if (!readsOnly) {
|
||||
logger.debug(`debugging: '${name}' written to`, newVal, getTrace())
|
||||
logger().debug(`debugging: '${name}' written to`, newVal, getTrace())
|
||||
}
|
||||
|
||||
ref.value = newVal
|
||||
|
||||
@@ -195,7 +195,7 @@ export function updateCacheByFilter<TData, TVariables = unknown>(
|
||||
): boolean {
|
||||
const { fragment, query } = filter
|
||||
const { ignoreCacheErrors = true, overwrite = true } = options
|
||||
const logger = useStrictLoggerSync()
|
||||
const { logger } = useSafeLogger()
|
||||
|
||||
if (!fragment && !query) {
|
||||
throw new Error(
|
||||
@@ -242,7 +242,7 @@ export function updateCacheByFilter<TData, TVariables = unknown>(
|
||||
}
|
||||
|
||||
if (ignoreCacheErrors) {
|
||||
logger.warn('Failed Apollo cache update:', e)
|
||||
logger().warn('Failed Apollo cache update:', e)
|
||||
return false
|
||||
}
|
||||
throw e
|
||||
@@ -382,13 +382,13 @@ export function modifyObjectFields<
|
||||
) {
|
||||
const { fieldNameWhitelist, debug = false } = options || {}
|
||||
|
||||
const logger = useStrictLoggerSync()
|
||||
const { logger } = useSafeLogger()
|
||||
const invocationId = nanoid()
|
||||
const log = (...args: Parameters<typeof logger.debug>) => {
|
||||
const log = (...args: unknown[]) => {
|
||||
if (!debug) return
|
||||
const [message, ...rest] = args
|
||||
|
||||
logger.debug(`[${invocationId}] ${message}`, ...rest)
|
||||
logger().debug(`[${invocationId}] ${message}`, ...rest)
|
||||
}
|
||||
|
||||
log(
|
||||
|
||||
@@ -203,7 +203,7 @@ export const doesRouteFitTarget = (fullPathA: string, fullPathB: string) => {
|
||||
urlA = new URL(fullPathA, fakeOrigin)
|
||||
urlB = new URL(fullPathB, fakeOrigin)
|
||||
} catch (e) {
|
||||
useStrictLoggerSync().warn('Failed to parse URLs', e)
|
||||
useSafeLogger().logger().warn('Failed to parse URLs', e)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export function isEmbedOptions(obj: unknown): obj is EmbedOptions {
|
||||
}
|
||||
|
||||
export function deserializeEmbedOptions(embedString: string | null): EmbedOptions {
|
||||
const logger = useStrictLoggerSync()
|
||||
const { logger } = useSafeLogger()
|
||||
if (!embedString) {
|
||||
return { isEnabled: false }
|
||||
}
|
||||
@@ -47,9 +47,9 @@ export function deserializeEmbedOptions(embedString: string | null): EmbedOption
|
||||
if (isEmbedOptions(parsed)) {
|
||||
return { ...parsed, isEnabled: true }
|
||||
}
|
||||
logger.error('Parsed object is not of type EmbedOptions')
|
||||
logger().error('Parsed object is not of type EmbedOptions')
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
logger().error(error)
|
||||
}
|
||||
|
||||
return { isEnabled: false }
|
||||
|
||||
Reference in New Issue
Block a user