Add global log options to CLI (#3519)

### Description

- Add global log options to CLI
- Improve consistency with agent log options
- Other minor log-related cleanup

### Related issues

Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3499

### Backward compatibility

Yes because the new log options haven't yet published
pull/3522/head
J M Rossy 8 months ago committed by GitHub
parent 41cc7f5c4b
commit 5373d54ca5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/shy-beans-deny.md
  2. 5
      .github/workflows/cron.yml
  3. 3
      .github/workflows/test.yml
  4. 10
      README.md
  5. 5
      typescript/cli/README.md
  6. 4
      typescript/cli/ci-test.sh
  7. 14
      typescript/cli/cli.ts
  8. 3
      typescript/cli/env.ts
  9. 14
      typescript/cli/src/commands/options.ts
  10. 21
      typescript/cli/src/logger.ts
  11. 4
      typescript/sdk/src/deploy/HyperlaneDeployer.ts
  12. 14
      typescript/utils/index.ts
  13. 11
      typescript/utils/src/env.ts
  14. 134
      typescript/utils/src/logging.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': patch
---
Add --log and --verbosity settings to CLI

@ -7,7 +7,8 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
DEBUG: 'hyperlane:*' LOG_LEVEL: DEBUG
LOG_FORMAT: PRETTY
jobs: jobs:
# copied from test.yml # copied from test.yml
@ -91,7 +92,7 @@ jobs:
key: ${{ github.sha }} key: ${{ github.sha }}
- name: Metadata Health Check - name: Metadata Health Check
run: LOG_PRETTY=true yarn workspace @hyperlane-xyz/sdk run test:metadata run: yarn workspace @hyperlane-xyz/sdk run test:metadata
- name: Post to discord webhook if metadata check fails - name: Post to discord webhook if metadata check fails
if: failure() if: failure()

@ -14,7 +14,8 @@ concurrency:
cancel-in-progress: ${{ github.ref_name != 'main' }} cancel-in-progress: ${{ github.ref_name != 'main' }}
env: env:
DEBUG: 'hyperlane:*' LOG_LEVEL: DEBUG
LOG_FORMAT: PRETTY
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
RUST_BACKTRACE: full RUST_BACKTRACE: full

@ -64,6 +64,16 @@ This monorepo uses [Yarn Workspaces](https://yarnpkg.com/features/workspaces). I
If you are using [VSCode](https://code.visualstudio.com/), you can launch the [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) with `code mono.code-workspace`, install the recommended workspace extensions, and use the editor settings. If you are using [VSCode](https://code.visualstudio.com/), you can launch the [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) with `code mono.code-workspace`, install the recommended workspace extensions, and use the editor settings.
### Logging
The typescript tooling uses [Pino](https://github.com/pinojs/pino) based logging, which outputs structured JSON logs by default.
The verbosity level and style can be configured with environment variables:
```sh
LOG_LEVEL=DEBUG|INFO|WARN|ERROR|OFF
LOG_FORMAT=PRETTY|JSON
```
### Rust ### Rust
See [`rust/README.md`](rust/README.md) See [`rust/README.md`](rust/README.md)

@ -55,3 +55,8 @@ Run warp route deployments: `hyperlane deploy warp`
View SDK contract addresses: `hyperlane chains addresses` View SDK contract addresses: `hyperlane chains addresses`
Send test message: `hyperlane send message` Send test message: `hyperlane send message`
## Logging
The logging format can be toggled between human-readable vs JSON-structured logs using the `LOG_FORMAT` environment variable or the `--log <pretty|json>` flag.
The logging verbosity can be configured using the `LOG_LEVEL` environment variable or the `--verbosity <debug|info|warn|error|off>` flag.

@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export LOG_LEVEL=DEBUG
# set script location as repo root # set script location as repo root
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
@ -87,8 +89,6 @@ set -e
echo "{}" > /tmp/empty-artifacts.json echo "{}" > /tmp/empty-artifacts.json
export LOG_LEVEL=DEBUG
DEPLOYER=$(cast rpc eth_accounts | jq -r '.[0]') DEPLOYER=$(cast rpc eth_accounts | jq -r '.[0]')
BEFORE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT}) BEFORE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})

@ -2,13 +2,19 @@
import chalk from 'chalk'; import chalk from 'chalk';
import yargs from 'yargs'; import yargs from 'yargs';
import type { LogFormat, LogLevel } from '@hyperlane-xyz/utils';
import './env.js'; import './env.js';
import { chainsCommand } from './src/commands/chains.js'; import { chainsCommand } from './src/commands/chains.js';
import { configCommand } from './src/commands/config.js'; import { configCommand } from './src/commands/config.js';
import { deployCommand } from './src/commands/deploy.js'; import { deployCommand } from './src/commands/deploy.js';
import {
logFormatCommandOption,
logLevelCommandOption,
} from './src/commands/options.js';
import { sendCommand } from './src/commands/send.js'; import { sendCommand } from './src/commands/send.js';
import { statusCommand } from './src/commands/status.js'; import { statusCommand } from './src/commands/status.js';
import { errorRed } from './src/logger.js'; import { configureLogger, errorRed } from './src/logger.js';
import { checkVersion } from './src/utils/version-check.js'; import { checkVersion } from './src/utils/version-check.js';
import { VERSION } from './src/version.js'; import { VERSION } from './src/version.js';
@ -22,6 +28,12 @@ await checkVersion();
try { try {
await yargs(process.argv.slice(2)) await yargs(process.argv.slice(2))
.scriptName('hyperlane') .scriptName('hyperlane')
.option('log', logFormatCommandOption)
.option('verbosity', logLevelCommandOption)
.global(['log', 'verbosity'])
.middleware((argv) => {
configureLogger(argv.log as LogFormat, argv.verbosity as LogLevel);
})
.command(chainsCommand) .command(chainsCommand)
.command(configCommand) .command(configCommand)
.command(deployCommand) .command(deployCommand)

@ -1,9 +1,6 @@
// This file isn't in the src dir so it it's imported before others // This file isn't in the src dir so it it's imported before others
// See https://github.com/trivago/prettier-plugin-sort-imports/issues/112 // See https://github.com/trivago/prettier-plugin-sort-imports/issues/112
// Default rootLogger into pretty mode
process.env.LOG_PRETTY ??= 'true';
// Workaround for bug in bigint-buffer which solana-web3.js depends on // Workaround for bug in bigint-buffer which solana-web3.js depends on
// https://github.com/no2chem/bigint-buffer/issues/31#issuecomment-1752134062 // https://github.com/no2chem/bigint-buffer/issues/31#issuecomment-1752134062
const defaultWarn = console.warn; const defaultWarn = console.warn;

@ -1,6 +1,20 @@
// A set of common options // A set of common options
import { Options } from 'yargs'; import { Options } from 'yargs';
import { LogFormat, LogLevel } from '@hyperlane-xyz/utils';
export const logFormatCommandOption: Options = {
type: 'string',
description: 'Log output format',
choices: Object.values(LogFormat),
};
export const logLevelCommandOption: Options = {
type: 'string',
description: 'Log verbosity level',
choices: Object.values(LogLevel),
};
export const keyCommandOption: Options = { export const keyCommandOption: Options = {
type: 'string', type: 'string',
description: description:

@ -1,9 +1,24 @@
import chalk, { ChalkInstance } from 'chalk'; import chalk, { ChalkInstance } from 'chalk';
import { pino } from 'pino'; import { pino } from 'pino';
import { isLogPretty, rootLogger } from '@hyperlane-xyz/utils'; import {
LogFormat,
LogLevel,
configureRootLogger,
getLogFormat,
rootLogger,
safelyAccessEnvVar,
} from '@hyperlane-xyz/utils';
let logger = rootLogger;
export function configureLogger(logFormat: LogFormat, logLevel: LogLevel) {
logFormat =
logFormat || safelyAccessEnvVar('LOG_FORMAT', true) || LogFormat.Pretty;
logLevel = logLevel || safelyAccessEnvVar('LOG_LEVEL', true) || LogLevel.Info;
logger = configureRootLogger(logFormat, logLevel).child({ module: 'cli' });
}
export const logger = rootLogger.child({ module: 'cli' });
export const log = (msg: string, ...args: any) => logger.info(msg, ...args); export const log = (msg: string, ...args: any) => logger.info(msg, ...args);
export function logColor( export function logColor(
@ -12,7 +27,7 @@ export function logColor(
...args: any ...args: any
) { ) {
// Only use color when pretty is enabled // Only use color when pretty is enabled
if (isLogPretty) { if (getLogFormat() === LogFormat.Pretty) {
logger[level](chalkInstance(...args)); logger[level](chalkInstance(...args));
} else { } else {
// @ts-ignore pino type more restrictive than pino's actual arg handling // @ts-ignore pino type more restrictive than pino's actual arg handling

@ -119,7 +119,7 @@ export abstract class HyperlaneDeployer<
); );
const signerAddress = await this.multiProvider.getSignerAddress(chain); const signerAddress = await this.multiProvider.getSignerAddress(chain);
const fromString = signerUrl || signerAddress; const fromString = signerUrl || signerAddress;
this.logger.debug(`Deploying to ${chain} from ${fromString}`); this.logger.info(`Deploying to ${chain} from ${fromString}`);
this.startingBlockNumbers[chain] = await this.multiProvider this.startingBlockNumbers[chain] = await this.multiProvider
.getProvider(chain) .getProvider(chain)
.getBlockNumber(); .getBlockNumber();
@ -345,7 +345,7 @@ export abstract class HyperlaneDeployer<
} }
} }
this.logger.debug( this.logger.info(
`Deploy ${contractName} on ${chain} with constructor args (${constructorArgs.join( `Deploy ${contractName} on ${chain} with constructor args (${constructorArgs.join(
', ', ', ',
)})`, )})`,

@ -73,9 +73,19 @@ export {
isS3CheckpointWithId, isS3CheckpointWithId,
} from './src/checkpoints'; } from './src/checkpoints';
export { domainHash } from './src/domains'; export { domainHash } from './src/domains';
export { envVarToBoolean, safelyAccessEnvVar } from './src/env'; export { safelyAccessEnvVar } from './src/env';
export { canonizeId, evmId } from './src/ids'; export { canonizeId, evmId } from './src/ids';
export { isLogPretty, rootLogger } from './src/logging'; export {
LogFormat,
LogLevel,
configureRootLogger,
createHyperlanePinoLogger,
getLogFormat,
getLogLevel,
getRootLogger,
rootLogger,
setRootLogger,
} from './src/logging';
export { mean, median, stdDev, sum } from './src/math'; export { mean, median, stdDev, sum } from './src/math';
export { formatMessage, messageId, parseMessage } from './src/messages'; export { formatMessage, messageId, parseMessage } from './src/messages';
export { export {

@ -1,16 +1,9 @@
// Should be used instead of referencing process directly in case we don't // Should be used instead of referencing process directly in case we don't
// run in node.js // run in node.js
export function safelyAccessEnvVar(name: string) { export function safelyAccessEnvVar(name: string, toLowerCase = false) {
try { try {
return process.env[name]; return toLowerCase ? process.env[name]?.toLowerCase() : process.env[name];
} catch (error) { } catch (error) {
return undefined; return undefined;
} }
} }
export function envVarToBoolean(value: any) {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') return value.toLowerCase() === 'true';
if (typeof value === 'number') return value !== 0;
return !!value;
}

@ -1,38 +1,98 @@
import { LevelWithSilent, pino } from 'pino'; import { LevelWithSilent, Logger, pino } from 'pino';
import { envVarToBoolean, safelyAccessEnvVar } from './env'; import { safelyAccessEnvVar } from './env';
let logLevel: LevelWithSilent = 'info'; // Level and format here should correspond with the agent options as much as possible
const envLogLevel = safelyAccessEnvVar('LOG_LEVEL')?.toLowerCase(); // https://docs.hyperlane.xyz/docs/operate/config-reference#logfmt
if (envLogLevel && pino.levels.values[envLogLevel]) {
logLevel = envLogLevel as LevelWithSilent; // A custom enum definition because pino does not export an enum
} // and because we use 'off' instead of 'silent' to match the agent options
// For backwards compat and also to match agent level options export enum LogLevel {
else if (envLogLevel === 'none' || envLogLevel === 'off') { Debug = 'debug',
logLevel = 'silent'; Info = 'info',
} Warn = 'warn',
Error = 'error',
export const isLogPretty = envVarToBoolean(safelyAccessEnvVar('LOG_PRETTY')); Off = 'off',
}
export const rootLogger = pino({
level: logLevel, let logLevel: LevelWithSilent =
name: 'hyperlane', toPinoLevel(safelyAccessEnvVar('LOG_LEVEL', true)) || 'info';
formatters: {
// Remove pino's default bindings of hostname but keep pid function toPinoLevel(level?: string): LevelWithSilent | undefined {
bindings: (defaultBindings) => ({ pid: defaultBindings.pid }), if (level && pino.levels.values[level]) return level as LevelWithSilent;
}, // For backwards compat and also to match agent level options
hooks: { else if (level === 'none' || level === 'off') return 'silent';
logMethod(inputArgs, method, level) { else return undefined;
// Pino has no simple way of setting custom log shapes and they }
// recommend against using pino-pretty in production so when
// pretty is enabled we circumvent pino and log directly to console export function getLogLevel() {
if (isLogPretty && level >= pino.levels.values[logLevel]) { return logLevel;
// eslint-disable-next-line no-console }
console.log(...inputArgs);
// Then return null to prevent pino from logging export enum LogFormat {
return null; Pretty = 'pretty',
} JSON = 'json',
return method.apply(this, inputArgs); }
let logFormat: LogFormat = LogFormat.JSON;
const envLogFormat = safelyAccessEnvVar('LOG_FORMAT', true) as
| LogFormat
| undefined;
if (envLogFormat && Object.values(LogFormat).includes(envLogFormat))
logFormat = envLogFormat;
export function getLogFormat() {
return logFormat;
}
// Note, for brevity and convenience, the rootLogger is exported directly
export let rootLogger = createHyperlanePinoLogger(logLevel, logFormat);
export function getRootLogger() {
return rootLogger;
}
export function configureRootLogger(
newLogFormat: LogFormat,
newLogLevel: LogLevel,
) {
logFormat = newLogFormat;
logLevel = toPinoLevel(newLogLevel) || logLevel;
rootLogger = createHyperlanePinoLogger(logLevel, logFormat);
return rootLogger;
}
export function setRootLogger(logger: Logger) {
rootLogger = logger;
return rootLogger;
}
export function createHyperlanePinoLogger(
logLevel: LevelWithSilent,
logFormat: LogFormat,
) {
return pino({
level: logLevel,
name: 'hyperlane',
formatters: {
// Remove pino's default bindings of hostname but keep pid
bindings: (defaultBindings) => ({ pid: defaultBindings.pid }),
}, },
}, hooks: {
}); logMethod(inputArgs, method, level) {
// Pino has no simple way of setting custom log shapes and they
// recommend against using pino-pretty in production so when
// pretty is enabled we circumvent pino and log directly to console
if (
logFormat === LogFormat.Pretty &&
level >= pino.levels.values[logLevel]
) {
// eslint-disable-next-line no-console
console.log(...inputArgs);
// Then return null to prevent pino from logging
return null;
}
return method.apply(this, inputArgs);
},
},
});
}

Loading…
Cancel
Save