Skip to content

Commit

Permalink
Implement sdk log watching and progress on log modal
Browse files Browse the repository at this point in the history
  • Loading branch information
jmrossy committed Jan 13, 2025
1 parent b2fcf74 commit e0a2794
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/consts/consts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const MIN_CHAIN_BALANCE = 1; // 1 Wei
// TODO edit this based on experiments
export const WARP_DEPLOY_GAS_UNITS = BigInt(1e7);
export const REFUND_FEE_PADDING_FACTOR = 1.1;
export const REFUND_FEE_PADDING_FACTOR = 1.2;
export const MIN_DEPLOYER_BALANCE_TO_SHOW = BigInt(1e15); // 0.001 ETH
19 changes: 8 additions & 11 deletions src/features/deployment/warp/WarpDeploymentDeploy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MultiProtocolProvider, WarpCoreConfig } from '@hyperlane-xyz/sdk';
import { errorToString, sleep } from '@hyperlane-xyz/utils';
import { Button, Modal, SpinnerIcon, useModal } from '@hyperlane-xyz/widgets';
import { Button, SpinnerIcon, useModal } from '@hyperlane-xyz/widgets';
import { useMemo, useState } from 'react';
import { PlanetSpinner } from '../../../components/animation/PlanetSpinner';
import { SlideIn } from '../../../components/animation/SlideIn';
Expand All @@ -9,7 +9,6 @@ import { GasIcon } from '../../../components/icons/GasIcon';
import { LogsIcon } from '../../../components/icons/LogsIcon';
import { StopIcon } from '../../../components/icons/StopIcon';
import { H1 } from '../../../components/text/Headers';
import { config } from '../../../consts/config';
import { WARP_DEPLOY_GAS_UNITS } from '../../../consts/consts';
import { CardPage } from '../../../flows/CardPage';
import { useCardNav } from '../../../flows/hooks';
Expand All @@ -19,11 +18,13 @@ import { getChainDisplayName } from '../../chains/utils';
import { useFundDeployerAccount } from '../../deployerWallet/fund';
import { useRefundDeployerAccounts } from '../../deployerWallet/refund';
import { useOrCreateDeployerWallets } from '../../deployerWallet/wallets';
import { LogModal } from '../../logs/LogModal';
import { useSdkLogWatcher } from '../../logs/useSdkLogs';
import { useDeploymentHistory, useWarpDeploymentConfig } from '../hooks';
import { DeploymentStatus, DeploymentType, WarpDeploymentConfig } from '../types';
import { useWarpDeployment } from './deploy';

const CANCEL_SLEEP_DELAY = config.isDevMode ? 5_000 : 10_000;
const CANCEL_SLEEP_DELAY = 10_000;

enum DeployStep {
FundDeployer,
Expand All @@ -41,6 +42,8 @@ export function WarpDeploymentDeploy() {
const { updateDeploymentStatus, currentIndex, completeDeployment, failDeployment } =
useDeploymentHistory();

useSdkLogWatcher();

const { refundAsync } = useRefundDeployerAccounts();

const onFailure = (error: Error) => {
Expand Down Expand Up @@ -211,10 +214,6 @@ function ExecuteDeploy({
const chainListString = chains.map((c) => getChainDisplayName(multiProvider, c, true)).join(', ');

const { isOpen, open, close } = useModal();
const onClickViewLogs = () => {
// TODO get logs somehow
open();
};

return (
<div className="flex flex-col items-center text-center">
Expand All @@ -226,13 +225,11 @@ function ExecuteDeploy({
<p className="text-gray-700">This will take a few minutes</p>
{/* <p className="mt-3">TODO status text</p> */}
</div>
<Button onClick={onClickViewLogs} className="mt-3 gap-2.5">
<Button onClick={open} className="mt-3 gap-2.5">
<LogsIcon width={14} height={14} color={Color.accent['500']} />
<span className="text-md text-accent-500">View deployment logs</span>
</Button>
<Modal isOpen={isOpen} close={close} panelClassname="p-4">
<div>TODO logs here</div>
</Modal>
<LogModal isOpen={isOpen} close={close} />
</div>
);
}
Expand Down
31 changes: 31 additions & 0 deletions src/features/logs/LogModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Modal, SegmentedControl } from '@hyperlane-xyz/widgets';
import { pino } from 'pino';
import { useState } from 'react';
import { H3 } from '../../components/text/Headers';
import { LOG_LEVELS, useSdkLogs } from './useSdkLogs';

export function LogModal({ isOpen, close }: { isOpen: boolean; close: () => void }) {
const sdkLogs = useSdkLogs();
const [filterLevel, setFilterLevel] = useState(LOG_LEVELS[1]);
const filterLevelValue = getLevelValue(filterLevel);

return (
<Modal isOpen={isOpen} close={close} panelClassname="p-4">
<H3 className="text-center">Deployment Logs</H3>
<SegmentedControl options={LOG_LEVELS} onChange={(l) => setFilterLevel(l!)} />
<ul className="list-none space-y-0.5 text-sm">
{sdkLogs.map(([timestamp, level, message], i) =>
getLevelValue(level) >= filterLevelValue ? (
<li className="nth-child(even):bg-blue-500/5" key={i}>
{new Date(timestamp).toLocaleTimeString()}: {message}
</li>
) : null,
)}
</ul>
</Modal>
);
}

function getLevelValue(level: string): number {
return pino.levels.values[level] || 0;
}
86 changes: 86 additions & 0 deletions src/features/logs/useSdkLogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
configureRootLogger,
getRootLogger,
LogFormat,
LogLevel as LogLevelWithOff,
} from '@hyperlane-xyz/utils';
import { useInterval } from '@hyperlane-xyz/widgets';
import { Logger } from 'pino';
import { useCallback, useEffect, useState } from 'react';

export const LOG_LEVELS = Object.values(LogLevelWithOff).filter((l) => l !== LogLevelWithOff.Off);
export type LogLevel = (typeof LOG_LEVELS)[number];
// Tuple of timestamp, level, message
type Log = [number, LogLevel, string];
let logBuffer: Array<Log> = [];

export function useSdkLogWatcher() {
useEffect(() => {
// TODO confirm this line doesn't break the log watching
configureRootLogger(LogFormat.JSON, LogLevelWithOff.Debug);
const onLog = (timestamp: number, level: LogLevel, ...args: any) => {
const message = `${args}`.replaceAll('[object Object],', '').trim();
logBuffer.push([timestamp, level, message]);
};
const rootLogger = getRootLogger();
// NOTE ABOUT PINO:
// Pino sucks. Splitting it's log output to multiple transports doesn't seem
// to be possible. There is a way to specify transports at logger init time
// but it requires the use of worker threads which greatly complicates the
// bundling and runtime requirements for the utils lib. The following two
// method calls hack in wrappers for the log methods to force a call to onLog.
wrapChildMethod(rootLogger, onLog);
wrapLogMethods(rootLogger, onLog);

return () => {
// Replace global rootLogger with new one
// TODO this may not work since deployer files already got ran and bound to first rootLogger
configureRootLogger(LogFormat.JSON, LogLevelWithOff.Debug);
logBuffer = [];
};
}, []);
}

export function useSdkLogs() {
const [logs, setLogs] = useState<Array<Log>>([]);
const syncLogs = useCallback(() => setLogs([...logBuffer]), []);
useInterval(syncLogs, 250);
return logs;
}

/**
* Add a layer of indirection to the logger's 'child' method
* so that any children created from it have their log methods wrapped.
*/
function wrapChildMethod(
logger: Logger,
onLog: (timestamp: number, level: LogLevel, ...args: any) => void,
) {
const defaultChild = logger.child.bind(logger);
// @ts-ignore allow spread argument
logger.child = (...args: any) => {
// @ts-ignore allow spread argument
const childLogger = defaultChild(...args);
wrapLogMethods(childLogger, onLog);
return childLogger;
};
}

/**
* Add a layer of indirection to the logger's log methods
* so that they trigger the onLog callback each time they're called.
*/
function wrapLogMethods(
logger: Logger,
onLog: (timestamp: number, level: LogLevel, ...args: any) => void,
) {
for (const level of LOG_LEVELS) {
const defaultMethod = logger[level].bind(logger);
const wrappedMethod = (...args: any) => {
// @ts-ignore allow spread argument
defaultMethod(...args);
onLog(Date.now(), level, ...args);
};
logger[level] = wrappedMethod;
}
}

0 comments on commit e0a2794

Please sign in to comment.