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:
env:
DEBUG: 'hyperlane:*'
LOG_LEVEL: DEBUG
LOG_FORMAT: PRETTY
jobs:
# copied from test.yml
@ -91,7 +92,7 @@ jobs:
key: ${{ github.sha }}
- 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
if: failure()

@ -14,7 +14,8 @@ concurrency:
cancel-in-progress: ${{ github.ref_name != 'main' }}
env:
DEBUG: 'hyperlane:*'
LOG_LEVEL: DEBUG
LOG_FORMAT: PRETTY
CARGO_TERM_COLOR: always
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.
### 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
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`
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
export LOG_LEVEL=DEBUG
# set script location as repo root
cd "$(dirname "$0")/../.."
@ -87,8 +89,6 @@ set -e
echo "{}" > /tmp/empty-artifacts.json
export LOG_LEVEL=DEBUG
DEPLOYER=$(cast rpc eth_accounts | jq -r '.[0]')
BEFORE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})

@ -2,13 +2,19 @@
import chalk from 'chalk';
import yargs from 'yargs';
import type { LogFormat, LogLevel } from '@hyperlane-xyz/utils';
import './env.js';
import { chainsCommand } from './src/commands/chains.js';
import { configCommand } from './src/commands/config.js';
import { deployCommand } from './src/commands/deploy.js';
import {
logFormatCommandOption,
logLevelCommandOption,
} from './src/commands/options.js';
import { sendCommand } from './src/commands/send.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 { VERSION } from './src/version.js';
@ -22,6 +28,12 @@ await checkVersion();
try {
await yargs(process.argv.slice(2))
.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(configCommand)
.command(deployCommand)

@ -1,9 +1,6 @@
// 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
// Default rootLogger into pretty mode
process.env.LOG_PRETTY ??= 'true';
// Workaround for bug in bigint-buffer which solana-web3.js depends on
// https://github.com/no2chem/bigint-buffer/issues/31#issuecomment-1752134062
const defaultWarn = console.warn;

@ -1,6 +1,20 @@
// A set of common options
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 = {
type: 'string',
description:

@ -1,9 +1,24 @@
import chalk, { ChalkInstance } from 'chalk';
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 function logColor(
@ -12,7 +27,7 @@ export function logColor(
...args: any
) {
// Only use color when pretty is enabled
if (isLogPretty) {
if (getLogFormat() === LogFormat.Pretty) {
logger[level](chalkInstance(...args));
} else {
// @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 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
.getProvider(chain)
.getBlockNumber();
@ -345,7 +345,7 @@ export abstract class HyperlaneDeployer<
}
}
this.logger.debug(
this.logger.info(
`Deploy ${contractName} on ${chain} with constructor args (${constructorArgs.join(
', ',
)})`,

@ -73,9 +73,19 @@ export {
isS3CheckpointWithId,
} from './src/checkpoints';
export { domainHash } from './src/domains';
export { envVarToBoolean, safelyAccessEnvVar } from './src/env';
export { safelyAccessEnvVar } from './src/env';
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 { formatMessage, messageId, parseMessage } from './src/messages';
export {

@ -1,16 +1,9 @@
// Should be used instead of referencing process directly in case we don't
// run in node.js
export function safelyAccessEnvVar(name: string) {
export function safelyAccessEnvVar(name: string, toLowerCase = false) {
try {
return process.env[name];
return toLowerCase ? process.env[name]?.toLowerCase() : process.env[name];
} catch (error) {
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 { envVarToBoolean, safelyAccessEnvVar } from './env';
let logLevel: LevelWithSilent = 'info';
const envLogLevel = safelyAccessEnvVar('LOG_LEVEL')?.toLowerCase();
if (envLogLevel && pino.levels.values[envLogLevel]) {
logLevel = envLogLevel as LevelWithSilent;
}
// For backwards compat and also to match agent level options
else if (envLogLevel === 'none' || envLogLevel === 'off') {
logLevel = 'silent';
}
export const isLogPretty = envVarToBoolean(safelyAccessEnvVar('LOG_PRETTY'));
export const rootLogger = 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 (isLogPretty && 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);
import { LevelWithSilent, Logger, pino } from 'pino';
import { safelyAccessEnvVar } from './env';
// Level and format here should correspond with the agent options as much as possible
// https://docs.hyperlane.xyz/docs/operate/config-reference#logfmt
// A custom enum definition because pino does not export an enum
// and because we use 'off' instead of 'silent' to match the agent options
export enum LogLevel {
Debug = 'debug',
Info = 'info',
Warn = 'warn',
Error = 'error',
Off = 'off',
}
let logLevel: LevelWithSilent =
toPinoLevel(safelyAccessEnvVar('LOG_LEVEL', true)) || 'info';
function toPinoLevel(level?: string): LevelWithSilent | undefined {
if (level && pino.levels.values[level]) return level as LevelWithSilent;
// For backwards compat and also to match agent level options
else if (level === 'none' || level === 'off') return 'silent';
else return undefined;
}
export function getLogLevel() {
return logLevel;
}
export enum LogFormat {
Pretty = 'pretty',
JSON = 'json',
}
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