Skip to content

Commit

Permalink
Add global log options to CLI (#3519)
Browse files Browse the repository at this point in the history
### Description

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

### Related issues

Fixes #3499

### Backward compatibility

Yes because the new log options haven't yet published
  • Loading branch information
jmrossy authored Mar 29, 2024
1 parent 41cc7f5 commit 5373d54
Show file tree
Hide file tree
Showing 14 changed files with 185 additions and 62 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-beans-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': patch
---

Add --log and --verbosity settings to CLI
5 changes: 3 additions & 2 deletions .github/workflows/cron.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ on:
workflow_dispatch:

env:
DEBUG: 'hyperlane:*'
LOG_LEVEL: DEBUG
LOG_FORMAT: PRETTY

jobs:
# copied from test.yml
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions typescript/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions typescript/cli/ci-test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env bash

export LOG_LEVEL=DEBUG

# set script location as repo root
cd "$(dirname "$0")/../.."

Expand Down Expand Up @@ -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})

Expand Down
14 changes: 13 additions & 1 deletion typescript/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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)
Expand Down
3 changes: 0 additions & 3 deletions typescript/cli/env.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
14 changes: 14 additions & 0 deletions typescript/cli/src/commands/options.ts
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
21 changes: 18 additions & 3 deletions typescript/cli/src/logger.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions typescript/sdk/src/deploy/HyperlaneDeployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -345,7 +345,7 @@ export abstract class HyperlaneDeployer<
}
}

this.logger.debug(
this.logger.info(
`Deploy ${contractName} on ${chain} with constructor args (${constructorArgs.join(
', ',
)})`,
Expand Down
14 changes: 12 additions & 2 deletions typescript/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 2 additions & 9 deletions typescript/utils/src/env.ts
Original file line number Diff line number Diff line change
@@ -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;
}
134 changes: 97 additions & 37 deletions typescript/utils/src/logging.ts
Original file line number Diff line number Diff line change
@@ -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);
},
},
});
}

0 comments on commit 5373d54

Please sign in to comment.