|
| 1 | +import { |
| 2 | + configureRootLogger, |
| 3 | + getRootLogger, |
| 4 | + LogFormat, |
| 5 | + LogLevel as LogLevelWithOff, |
| 6 | +} from '@hyperlane-xyz/utils'; |
| 7 | +import { useInterval } from '@hyperlane-xyz/widgets'; |
| 8 | +import { Logger } from 'pino'; |
| 9 | +import { useCallback, useEffect, useState } from 'react'; |
| 10 | + |
| 11 | +export const LOG_LEVELS = Object.values(LogLevelWithOff).filter((l) => l !== LogLevelWithOff.Off); |
| 12 | +export type LogLevel = (typeof LOG_LEVELS)[number]; |
| 13 | +// Tuple of timestamp, level, message |
| 14 | +type Log = [number, LogLevel, string]; |
| 15 | +let logBuffer: Array<Log> = []; |
| 16 | + |
| 17 | +export function useSdkLogWatcher() { |
| 18 | + useEffect(() => { |
| 19 | + // TODO confirm this line doesn't break the log watching |
| 20 | + configureRootLogger(LogFormat.JSON, LogLevelWithOff.Debug); |
| 21 | + const onLog = (timestamp: number, level: LogLevel, ...args: any) => { |
| 22 | + const message = `${args}`.replaceAll('[object Object],', '').trim(); |
| 23 | + logBuffer.push([timestamp, level, message]); |
| 24 | + }; |
| 25 | + const rootLogger = getRootLogger(); |
| 26 | + // NOTE ABOUT PINO: |
| 27 | + // Pino sucks. Splitting it's log output to multiple transports doesn't seem |
| 28 | + // to be possible. There is a way to specify transports at logger init time |
| 29 | + // but it requires the use of worker threads which greatly complicates the |
| 30 | + // bundling and runtime requirements for the utils lib. The following two |
| 31 | + // method calls hack in wrappers for the log methods to force a call to onLog. |
| 32 | + wrapChildMethod(rootLogger, onLog); |
| 33 | + wrapLogMethods(rootLogger, onLog); |
| 34 | + |
| 35 | + return () => { |
| 36 | + // Replace global rootLogger with new one |
| 37 | + // TODO this may not work since deployer files already got ran and bound to first rootLogger |
| 38 | + configureRootLogger(LogFormat.JSON, LogLevelWithOff.Debug); |
| 39 | + logBuffer = []; |
| 40 | + }; |
| 41 | + }, []); |
| 42 | +} |
| 43 | + |
| 44 | +export function useSdkLogs() { |
| 45 | + const [logs, setLogs] = useState<Array<Log>>([]); |
| 46 | + const syncLogs = useCallback(() => setLogs([...logBuffer]), []); |
| 47 | + useInterval(syncLogs, 250); |
| 48 | + return logs; |
| 49 | +} |
| 50 | + |
| 51 | +/** |
| 52 | + * Add a layer of indirection to the logger's 'child' method |
| 53 | + * so that any children created from it have their log methods wrapped. |
| 54 | + */ |
| 55 | +function wrapChildMethod( |
| 56 | + logger: Logger, |
| 57 | + onLog: (timestamp: number, level: LogLevel, ...args: any) => void, |
| 58 | +) { |
| 59 | + const defaultChild = logger.child.bind(logger); |
| 60 | + // @ts-ignore allow spread argument |
| 61 | + logger.child = (...args: any) => { |
| 62 | + // @ts-ignore allow spread argument |
| 63 | + const childLogger = defaultChild(...args); |
| 64 | + wrapLogMethods(childLogger, onLog); |
| 65 | + return childLogger; |
| 66 | + }; |
| 67 | +} |
| 68 | + |
| 69 | +/** |
| 70 | + * Add a layer of indirection to the logger's log methods |
| 71 | + * so that they trigger the onLog callback each time they're called. |
| 72 | + */ |
| 73 | +function wrapLogMethods( |
| 74 | + logger: Logger, |
| 75 | + onLog: (timestamp: number, level: LogLevel, ...args: any) => void, |
| 76 | +) { |
| 77 | + for (const level of LOG_LEVELS) { |
| 78 | + const defaultMethod = logger[level].bind(logger); |
| 79 | + const wrappedMethod = (...args: any) => { |
| 80 | + // @ts-ignore allow spread argument |
| 81 | + defaultMethod(...args); |
| 82 | + onLog(Date.now(), level, ...args); |
| 83 | + }; |
| 84 | + logger[level] = wrappedMethod; |
| 85 | + } |
| 86 | +} |
0 commit comments