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:
Kristaps Fabians Geikins
2025-08-21 13:58:22 +03:00
committed by GitHub
parent 8c745ad853
commit 4d67ac41b7
8 changed files with 103 additions and 75 deletions
+1 -1
View File
@@ -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
+82 -54
View File
@@ -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 }