From 8afea8c39a096c171b78f6cf75b9ab7f78cfc90d Mon Sep 17 00:00:00 2001 From: vponline Date: Thu, 12 Jan 2023 18:04:20 +0800 Subject: [PATCH 01/12] Write deployer logs and errors to file, throw general error for terraform command failures --- packages/airnode-deployer/src/cli/commands.ts | 32 ++++++++----------- .../src/infrastructure/index.test.ts | 14 ++++---- .../src/infrastructure/index.ts | 19 +++++++++-- .../src/utils/infrastructure.ts | 13 +++++++- packages/airnode-deployer/src/utils/logger.ts | 20 ++++++++++-- 5 files changed, 67 insertions(+), 31 deletions(-) diff --git a/packages/airnode-deployer/src/cli/commands.ts b/packages/airnode-deployer/src/cli/commands.ts index 7442af68a9..62bcdc00c1 100644 --- a/packages/airnode-deployer/src/cli/commands.ts +++ b/packages/airnode-deployer/src/cli/commands.ts @@ -1,6 +1,5 @@ import { loadConfig } from '@api3/airnode-node'; import { go } from '@api3/promise-utils'; -import { bold } from 'chalk'; import { deployAirnode, removeAirnode } from '../infrastructure'; import { writeReceiptFile, parseReceiptFile, parseSecretsFile } from '../utils'; import * as logger from '../utils/logger'; @@ -25,11 +24,10 @@ export async function deploy(configPath: string, secretsPath: string, receiptFil if (!goDeployAirnode.success && !autoRemove) { logger.fail( - bold( - `Airnode deployment failed due to unexpected errors.\n` + - ` It is possible that some resources have been deployed on cloud provider.\n` + - ` Please use the "remove" command from the deployer CLI to ensure all cloud resources are removed.` - ) + `Airnode deployment failed due to unexpected errors.\n` + + ` It is possible that some resources have been deployed on cloud provider.\n` + + ` Please use the "remove" command from the deployer CLI to ensure all cloud resources are removed.`, + { bold: true } ); throw goDeployAirnode.error; @@ -37,24 +35,22 @@ export async function deploy(configPath: string, secretsPath: string, receiptFil if (!goDeployAirnode.success) { logger.fail( - bold( - `Airnode deployment failed due to unexpected errors.\n` + - ` It is possible that some resources have been deployed on cloud provider.\n` + - ` Attempting to remove them...\n` - ) + `Airnode deployment failed due to unexpected errors.\n` + + ` It is possible that some resources have been deployed on cloud provider.\n` + + ` Attempting to remove them...\n`, + { bold: true } ); // Try to remove deployed resources const goRemoveAirnode = await go(() => removeWithReceipt(receiptFile)); if (!goRemoveAirnode.success) { logger.fail( - bold( - `Airnode removal failed due to unexpected errors.\n` + - ` It is possible that some resources have been deployed on cloud provider.\n` + - ` Please check the resources on the cloud provider dashboard and\n` + - ` use the "remove" command from the deployer CLI to remove them.\n` + - ` If the automatic removal via CLI fails, remove the resources manually.` - ) + `Airnode removal failed due to unexpected errors.\n` + + ` It is possible that some resources have been deployed on cloud provider.\n` + + ` Please check the resources on the cloud provider dashboard and\n` + + ` use the "remove" command from the deployer CLI to remove them.\n` + + ` If the automatic removal via CLI fails, remove the resources manually.`, + { bold: true } ); throw new MultiMessageError([ diff --git a/packages/airnode-deployer/src/infrastructure/index.test.ts b/packages/airnode-deployer/src/infrastructure/index.test.ts index d3279a1638..bdb6337ff1 100644 --- a/packages/airnode-deployer/src/infrastructure/index.test.ts +++ b/packages/airnode-deployer/src/infrastructure/index.test.ts @@ -810,11 +810,9 @@ describe('deployAirnode', () => { }); it(`throws an error if something in the deploy process wasn't successful`, async () => { - const expectedError = new Error('example error'); - exec.mockRejectedValue(expectedError); - + exec.mockRejectedValue('example error'); await expect(infrastructure.deployAirnode(config, configPath, secretsPath, Date.now())).rejects.toThrow( - expectedError.toString() + 'Terraform error occurred. See deployer-log.log and deployer-error.log files for more details.' ); }); }); @@ -1131,10 +1129,10 @@ describe('removeAirnode', () => { }); it('fails if the Terraform command fails', async () => { - const expectedError = new Error('example error'); - exec.mockRejectedValue(expectedError); - - await expect(infrastructure.removeAirnode(deploymentId)).rejects.toThrow(expectedError.toString()); + exec.mockRejectedValue('example error'); + await expect(infrastructure.removeAirnode(deploymentId)).rejects.toThrow( + 'Terraform error occurred. See deployer-log.log and deployer-error.log files for more details.' + ); }); }); diff --git a/packages/airnode-deployer/src/infrastructure/index.ts b/packages/airnode-deployer/src/infrastructure/index.ts index 9c599c91ee..1f1b83f5d4 100644 --- a/packages/airnode-deployer/src/infrastructure/index.ts +++ b/packages/airnode-deployer/src/infrastructure/index.ts @@ -29,6 +29,7 @@ import * as gcp from './gcp'; import * as logger from '../utils/logger'; import { logAndReturnError, + writeAndReturnError, formatTerraformArguments, getStageDirectory, getAddressDirectory, @@ -93,10 +94,24 @@ export async function runCommand(command: string, options: CommandOptions) { export type CommandArg = string | [string, string] | [string, string, string]; -export function execTerraform(execOptions: CommandOptions, command: string, args: CommandArg[], options?: string[]) { +export async function execTerraform( + execOptions: CommandOptions, + command: string, + args: CommandArg[], + options?: string[] +) { const formattedArgs = formatTerraformArguments(args); const fullCommand = compact(['terraform', command, formattedArgs.join(' '), options?.join(' ')]).join(' '); - return runCommand(fullCommand, execOptions); + + const goRunCommand = await go(() => runCommand(fullCommand, execOptions)); + if (!goRunCommand.success) { + throw writeAndReturnError( + goRunCommand.error, + 'Terraform error occurred. See deployer-log.log and deployer-error.log files for more details.' + ); + } + + return goRunCommand.data; } export function awsApplyDestroyArguments( diff --git a/packages/airnode-deployer/src/utils/infrastructure.ts b/packages/airnode-deployer/src/utils/infrastructure.ts index ff2ac16054..5baf0001db 100644 --- a/packages/airnode-deployer/src/utils/infrastructure.ts +++ b/packages/airnode-deployer/src/utils/infrastructure.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { randomBytes } from 'crypto'; import isArray from 'lodash/isArray'; import isEmpty from 'lodash/isEmpty'; @@ -31,11 +32,21 @@ export function formatTerraformArguments(args: CommandArg[]) { */ export const isCloudFunction = () => process.env.LAMBDA_TASK_ROOT || process.env.FUNCTION_TARGET; -export const logAndReturnError = (message: string): Error => { +export const logAndReturnError = (message: string) => { logger.fail(message); return new Error(message); }; +export const writeAndReturnError = (error: Error, message?: string) => { + fs.appendFileSync('config/deployer-error.log', `${new Date(Date.now()).toISOString()}: ${error.message}\n`); + + if (message) { + return new Error(message); + } + + return error; +}; + export class MultiMessageError extends Error { constructor(public messages: string[]) { super(messages.join('\n')); diff --git a/packages/airnode-deployer/src/utils/logger.ts b/packages/airnode-deployer/src/utils/logger.ts index 7548a46edc..38217960e7 100644 --- a/packages/airnode-deployer/src/utils/logger.ts +++ b/packages/airnode-deployer/src/utils/logger.ts @@ -1,4 +1,10 @@ +import fs from 'fs'; import * as ora from 'ora'; +import { bold } from 'chalk'; + +interface LoggerOptions { + bold: boolean; +} let debugModeFlag = false; const dummySpinner: ora.Ora = { @@ -27,15 +33,22 @@ function oraInstance(text?: string) { return debugModeFlag ? ora.default({ text, prefixText: () => new Date().toISOString() }) : ora.default(text); } +export function writeLog(text: string) { + fs.appendFileSync('config/deployer-log.log', `${new Date(Date.now()).toISOString()}: ${text}\n`); +} + export function succeed(text: string) { + writeLog(text); oraInstance().succeed(text); } -export function fail(text: string) { - oraInstance().fail(text); +export function fail(text: string, options: LoggerOptions = { bold: false }) { + writeLog(text); + oraInstance().fail(options.bold ? bold(text) : text); } export function warn(text: string) { + writeLog(text); const currentOra = getSpinner(); if (currentOra.isSpinning) { currentOra.clear(); @@ -45,6 +58,7 @@ export function warn(text: string) { } export function info(text: string) { + writeLog(text); const currentOra = getSpinner(); if (currentOra.isSpinning) { currentOra.clear(); @@ -54,10 +68,12 @@ export function info(text: string) { } export function debug(text: string) { + writeLog(text); if (debugModeFlag) info(text); } export function debugSpinner(text: string) { + writeLog(text); return debugModeFlag ? getSpinner().info(text) : dummySpinner; } From 5f78b773056afba723cb691380028048867f18a6 Mon Sep 17 00:00:00 2001 From: vponline Date: Thu, 12 Jan 2023 18:05:06 +0800 Subject: [PATCH 02/12] Add changeset --- .changeset/pink-beds-deny.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pink-beds-deny.md diff --git a/.changeset/pink-beds-deny.md b/.changeset/pink-beds-deny.md new file mode 100644 index 0000000000..479ab90ff0 --- /dev/null +++ b/.changeset/pink-beds-deny.md @@ -0,0 +1,5 @@ +--- +'@api3/airnode-deployer': patch +--- + +Write deployer logs and errors to file From 993fca00b0f950d0d2d63d0c4e09b46933bcb32e Mon Sep 17 00:00:00 2001 From: vponline Date: Tue, 17 Jan 2023 18:44:20 +0800 Subject: [PATCH 03/12] Refactor logs creation, add cli option for logs directory, hide secrets --- .changeset/pink-beds-deny.md | 2 +- packages/airnode-deployer/.gitignore | 3 ++ packages/airnode-deployer/src/cli/commands.ts | 10 ++++- packages/airnode-deployer/src/cli/index.ts | 44 ++++++++++++++++++- .../src/infrastructure/index.ts | 7 +-- .../src/utils/infrastructure.ts | 11 ----- packages/airnode-deployer/src/utils/logger.ts | 36 ++++++++++++--- 7 files changed, 87 insertions(+), 26 deletions(-) diff --git a/.changeset/pink-beds-deny.md b/.changeset/pink-beds-deny.md index 479ab90ff0..3d592ad7c5 100644 --- a/.changeset/pink-beds-deny.md +++ b/.changeset/pink-beds-deny.md @@ -2,4 +2,4 @@ '@api3/airnode-deployer': patch --- -Write deployer logs and errors to file +Write deployer logs to file diff --git a/packages/airnode-deployer/.gitignore b/packages/airnode-deployer/.gitignore index 999d242921..9b1bea7785 100644 --- a/packages/airnode-deployer/.gitignore +++ b/packages/airnode-deployer/.gitignore @@ -24,3 +24,6 @@ coverage/ .terraform* terraform.tfstate* *.tfvars + +# Logs +/logs \ No newline at end of file diff --git a/packages/airnode-deployer/src/cli/commands.ts b/packages/airnode-deployer/src/cli/commands.ts index 62bcdc00c1..472c452f76 100644 --- a/packages/airnode-deployer/src/cli/commands.ts +++ b/packages/airnode-deployer/src/cli/commands.ts @@ -64,8 +64,14 @@ export async function deploy(configPath: string, secretsPath: string, receiptFil } const output = goDeployAirnode.data; - if (output.httpGatewayUrl) logger.info(`HTTP gateway URL: ${output.httpGatewayUrl}`); - if (output.httpSignedDataGatewayUrl) logger.info(`HTTP signed data gateway URL: ${output.httpSignedDataGatewayUrl}`); + if (output.httpGatewayUrl) { + logger.setSecret(output.httpGatewayUrl); + logger.info(`HTTP gateway URL: ${output.httpGatewayUrl}`); + } + if (output.httpSignedDataGatewayUrl) { + logger.setSecret(output.httpSignedDataGatewayUrl); + logger.info(`HTTP signed data gateway URL: ${output.httpSignedDataGatewayUrl}`); + } } export async function removeWithReceipt(receiptFilename: string) { diff --git a/packages/airnode-deployer/src/cli/index.ts b/packages/airnode-deployer/src/cli/index.ts index ec86efd46d..6838d25001 100644 --- a/packages/airnode-deployer/src/cli/index.ts +++ b/packages/airnode-deployer/src/cli/index.ts @@ -46,7 +46,7 @@ async function runCommand(command: () => Promise) { } const cliExamples = [ - 'deploy -c config/config.json -s config/secrets.env -r config/receipt.json', + 'deploy -c config/config.json -s config/secrets.env -r config/receipt.json -l config/logs', 'list --cloud-providers gcp', 'info aws808e2a22', 'fetch-files aws808e2a22', @@ -82,6 +82,12 @@ yargs(hideBin(process.argv)) default: 'config/receipt.json', type: 'string', }, + logs: { + alias: 'l', + description: 'Output path for log files', + default: 'config/logs', + type: 'string', + }, // Flag arguments without value are not supported. See: https://github.com/yargs/yargs/issues/1532 'auto-remove': { description: 'Enable automatic removal of deployed resources for failed deployments', @@ -93,6 +99,7 @@ yargs(hideBin(process.argv)) drawHeader(); logger.debugMode(args.debug as boolean); + logger.setLogsDirectory(args.logs); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); await runCommand(() => deploy(args.configuration, args.secrets, args.receipt, args['auto-remove'])); } @@ -107,11 +114,18 @@ yargs(hideBin(process.argv)) default: 'config/receipt.json', type: 'string', }, + logs: { + alias: 'l', + description: 'Output path for log files', + default: 'config/logs', + type: 'string', + }, }, async (args) => { drawHeader(); logger.debugMode(args.debug as boolean); + logger.setLogsDirectory(args.logs); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); await runCommand(() => removeWithReceipt(args.receipt)); @@ -126,11 +140,18 @@ yargs(hideBin(process.argv)) description: `ID of the deployment (from 'list' command)`, type: 'string', }); + yargs.option('logs', { + alias: 'l', + description: 'Output path for log files', + default: 'config/logs', + type: 'string', + }); }, async (args) => { drawHeader(); logger.debugMode(args.debug as boolean); + logger.setLogsDirectory(args.logs as string); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); await runCommand(() => @@ -152,9 +173,16 @@ yargs(hideBin(process.argv)) type: 'array', coerce: (option: CloudProvider['type'][]) => sortBy(uniq(option)), }, + logs: { + alias: 'l', + description: 'Output path for log files', + default: 'config/logs', + type: 'string', + }, }, async (args) => { logger.debugMode(args.debug as boolean); + logger.setLogsDirectory(args.logs); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); await listAirnodes(args.cloudProviders); @@ -168,9 +196,16 @@ yargs(hideBin(process.argv)) description: `ID of the deployment (from 'list' command)`, type: 'string', }); + yargs.option('logs', { + alias: 'l', + description: 'Output path for log files', + default: 'config/logs', + type: 'string', + }); }, async (args) => { logger.debugMode(args.debug as boolean); + logger.setLogsDirectory(args.logs as string); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); // Looks like due to the bug in yargs (https://github.com/yargs/yargs/issues/1649) we need to specify the type explicitely @@ -200,9 +235,16 @@ yargs(hideBin(process.argv)) default: 'config/', type: 'string', }); + yargs.option('logs', { + alias: 'l', + description: 'Output path for log files', + default: 'config/logs', + type: 'string', + }); }, async (args) => { logger.debugMode(args.debug as boolean); + logger.setLogsDirectory(args.logs as string); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); // Looks like due to the bug in yargs (https://github.com/yargs/yargs/issues/1649) we need to specify the types explicitely diff --git a/packages/airnode-deployer/src/infrastructure/index.ts b/packages/airnode-deployer/src/infrastructure/index.ts index 1f1b83f5d4..a2bb3fc094 100644 --- a/packages/airnode-deployer/src/infrastructure/index.ts +++ b/packages/airnode-deployer/src/infrastructure/index.ts @@ -29,7 +29,6 @@ import * as gcp from './gcp'; import * as logger from '../utils/logger'; import { logAndReturnError, - writeAndReturnError, formatTerraformArguments, getStageDirectory, getAddressDirectory, @@ -105,10 +104,7 @@ export async function execTerraform( const goRunCommand = await go(() => runCommand(fullCommand, execOptions)); if (!goRunCommand.success) { - throw writeAndReturnError( - goRunCommand.error, - 'Terraform error occurred. See deployer-log.log and deployer-error.log files for more details.' - ); + throw new Error('Terraform error occurred. See deployer log files for more details.'); } return goRunCommand.data; @@ -263,6 +259,7 @@ export async function terraformAirnodeApply( const cloudProvider = config.nodeSettings.cloudProvider as CloudProvider; const airnodeAddress = deriveAirnodeAddress(airnodeWalletMnemonic); const airnodeWalletPrivateKey = evm.getAirnodeWallet(config).privateKey; + logger.setSecret(airnodeWalletPrivateKey); const maxConcurrency = config.chains.reduce((concurrency: number, chain) => concurrency + chain.maxConcurrency, 0); await terraformAirnodeInit(execOptions, cloudProvider, bucket, bucketDeploymentPath); diff --git a/packages/airnode-deployer/src/utils/infrastructure.ts b/packages/airnode-deployer/src/utils/infrastructure.ts index 5baf0001db..0b17c3f01a 100644 --- a/packages/airnode-deployer/src/utils/infrastructure.ts +++ b/packages/airnode-deployer/src/utils/infrastructure.ts @@ -1,4 +1,3 @@ -import fs from 'fs'; import { randomBytes } from 'crypto'; import isArray from 'lodash/isArray'; import isEmpty from 'lodash/isEmpty'; @@ -37,16 +36,6 @@ export const logAndReturnError = (message: string) => { return new Error(message); }; -export const writeAndReturnError = (error: Error, message?: string) => { - fs.appendFileSync('config/deployer-error.log', `${new Date(Date.now()).toISOString()}: ${error.message}\n`); - - if (message) { - return new Error(message); - } - - return error; -}; - export class MultiMessageError extends Error { constructor(public messages: string[]) { super(messages.join('\n')); diff --git a/packages/airnode-deployer/src/utils/logger.ts b/packages/airnode-deployer/src/utils/logger.ts index 38217960e7..05715ceca5 100644 --- a/packages/airnode-deployer/src/utils/logger.ts +++ b/packages/airnode-deployer/src/utils/logger.ts @@ -1,11 +1,16 @@ import fs from 'fs'; import * as ora from 'ora'; import { bold } from 'chalk'; +import { format } from 'date-fns-tz'; -interface LoggerOptions { - bold: boolean; +export interface LoggerOptions { + bold?: boolean; } +let logsDirectory = 'config/logs'; +let logFileTimestamp: string; +const secrets: string[] = []; + let debugModeFlag = false; const dummySpinner: ora.Ora = { ...ora.default(), @@ -34,7 +39,9 @@ function oraInstance(text?: string) { } export function writeLog(text: string) { - fs.appendFileSync('config/deployer-log.log', `${new Date(Date.now()).toISOString()}: ${text}\n`); + const timestamp = format(Date.now(), 'yyyy-MM-dd HH:mm:ss'); + const sanitizedLogs = replaceSecrets(text, secrets); + fs.appendFileSync(`${logsDirectory}/deployer-${logFileTimestamp}.log`, `${timestamp}: ${sanitizedLogs}\n`); } export function succeed(text: string) { @@ -42,9 +49,9 @@ export function succeed(text: string) { oraInstance().succeed(text); } -export function fail(text: string, options: LoggerOptions = { bold: false }) { +export function fail(text: string, options?: LoggerOptions) { writeLog(text); - oraInstance().fail(options.bold ? bold(text) : text); + oraInstance().fail(options?.bold ? bold(text) : text); } export function warn(text: string) { @@ -68,7 +75,6 @@ export function info(text: string) { } export function debug(text: string) { - writeLog(text); if (debugModeFlag) info(text); } @@ -84,3 +90,21 @@ export function debugMode(mode: boolean) { export function inDebugMode() { return debugModeFlag; } + +export function setSecret(secret: string) { + secrets.push(secret); +} + +export function replaceSecrets(input: string, secrets: string[]) { + let output = input; + secrets.forEach((secret) => (output = output.replace(secret, '*'.repeat(secret.length)))); + + return output; +} + +export function setLogsDirectory(path: string) { + logsDirectory = path.endsWith('/') ? path.slice(0, -1) : path; + if (!fs.existsSync(logsDirectory)) fs.mkdirSync(logsDirectory, { recursive: true }); + + logFileTimestamp = format(Date.now(), 'yyyy-MM-dd_HH:mm:ss'); +} From 211a23fa6d16f8fba573b6f3d38858255c56b04f Mon Sep 17 00:00:00 2001 From: vponline Date: Tue, 17 Jan 2023 19:54:13 +0800 Subject: [PATCH 04/12] Mock writeLog in tests --- packages/airnode-deployer/src/infrastructure/index.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/airnode-deployer/src/infrastructure/index.test.ts b/packages/airnode-deployer/src/infrastructure/index.test.ts index bdb6337ff1..032b22495e 100644 --- a/packages/airnode-deployer/src/infrastructure/index.test.ts +++ b/packages/airnode-deployer/src/infrastructure/index.test.ts @@ -18,6 +18,11 @@ jest.mock('../../package.json', () => ({ version: '0.8.0', })); +jest.mock('../utils/logger', () => ({ + ...jest.requireActual('../utils/logger'), + writeLog: jest.fn(), +})); + const exec = jest.fn(); jest.spyOn(util, 'promisify').mockImplementation(() => exec); From 3c6e9333be610df43858dd9f17decfec35a1398c Mon Sep 17 00:00:00 2001 From: vponline Date: Tue, 17 Jan 2023 23:29:47 +0800 Subject: [PATCH 05/12] Replace mocking writeLog with fs.appendFileSync --- .../airnode-deployer/src/infrastructure/index.test.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/airnode-deployer/src/infrastructure/index.test.ts b/packages/airnode-deployer/src/infrastructure/index.test.ts index 032b22495e..9aa76efb08 100644 --- a/packages/airnode-deployer/src/infrastructure/index.test.ts +++ b/packages/airnode-deployer/src/infrastructure/index.test.ts @@ -18,13 +18,9 @@ jest.mock('../../package.json', () => ({ version: '0.8.0', })); -jest.mock('../utils/logger', () => ({ - ...jest.requireActual('../utils/logger'), - writeLog: jest.fn(), -})); - const exec = jest.fn(); jest.spyOn(util, 'promisify').mockImplementation(() => exec); +jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn()); import { version as nodeVersion } from '../../package.json'; import * as infrastructure from '.'; @@ -817,7 +813,7 @@ describe('deployAirnode', () => { it(`throws an error if something in the deploy process wasn't successful`, async () => { exec.mockRejectedValue('example error'); await expect(infrastructure.deployAirnode(config, configPath, secretsPath, Date.now())).rejects.toThrow( - 'Terraform error occurred. See deployer-log.log and deployer-error.log files for more details.' + 'Terraform error occurred. See deployer log files for more details.' ); }); }); @@ -1136,7 +1132,7 @@ describe('removeAirnode', () => { it('fails if the Terraform command fails', async () => { exec.mockRejectedValue('example error'); await expect(infrastructure.removeAirnode(deploymentId)).rejects.toThrow( - 'Terraform error occurred. See deployer-log.log and deployer-error.log files for more details.' + 'Terraform error occurred. See deployer log files for more details.' ); }); }); From f83319462b3925881c6f27043115cc8491982c96 Mon Sep 17 00:00:00 2001 From: vponline Date: Wed, 18 Jan 2023 16:40:49 +0800 Subject: [PATCH 06/12] Add trailing slash to logs default dir for consistency --- packages/airnode-deployer/src/cli/index.ts | 14 +++++++------- packages/airnode-deployer/src/utils/logger.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/airnode-deployer/src/cli/index.ts b/packages/airnode-deployer/src/cli/index.ts index 6838d25001..14cd222ee6 100644 --- a/packages/airnode-deployer/src/cli/index.ts +++ b/packages/airnode-deployer/src/cli/index.ts @@ -46,7 +46,7 @@ async function runCommand(command: () => Promise) { } const cliExamples = [ - 'deploy -c config/config.json -s config/secrets.env -r config/receipt.json -l config/logs', + 'deploy -c config/config.json -s config/secrets.env -r config/receipt.json -l config/logs/', 'list --cloud-providers gcp', 'info aws808e2a22', 'fetch-files aws808e2a22', @@ -85,7 +85,7 @@ yargs(hideBin(process.argv)) logs: { alias: 'l', description: 'Output path for log files', - default: 'config/logs', + default: 'config/logs/', type: 'string', }, // Flag arguments without value are not supported. See: https://github.com/yargs/yargs/issues/1532 @@ -117,7 +117,7 @@ yargs(hideBin(process.argv)) logs: { alias: 'l', description: 'Output path for log files', - default: 'config/logs', + default: 'config/logs/', type: 'string', }, }, @@ -143,7 +143,7 @@ yargs(hideBin(process.argv)) yargs.option('logs', { alias: 'l', description: 'Output path for log files', - default: 'config/logs', + default: 'config/logs/', type: 'string', }); }, @@ -176,7 +176,7 @@ yargs(hideBin(process.argv)) logs: { alias: 'l', description: 'Output path for log files', - default: 'config/logs', + default: 'config/logs/', type: 'string', }, }, @@ -199,7 +199,7 @@ yargs(hideBin(process.argv)) yargs.option('logs', { alias: 'l', description: 'Output path for log files', - default: 'config/logs', + default: 'config/logs/', type: 'string', }); }, @@ -238,7 +238,7 @@ yargs(hideBin(process.argv)) yargs.option('logs', { alias: 'l', description: 'Output path for log files', - default: 'config/logs', + default: 'config/logs/', type: 'string', }); }, diff --git a/packages/airnode-deployer/src/utils/logger.ts b/packages/airnode-deployer/src/utils/logger.ts index 05715ceca5..a50492006a 100644 --- a/packages/airnode-deployer/src/utils/logger.ts +++ b/packages/airnode-deployer/src/utils/logger.ts @@ -7,7 +7,7 @@ export interface LoggerOptions { bold?: boolean; } -let logsDirectory = 'config/logs'; +let logsDirectory = 'config/logs/'; let logFileTimestamp: string; const secrets: string[] = []; From b8efcf0a02e461fce000c3839b0f4ce005c58790 Mon Sep 17 00:00:00 2001 From: vponline Date: Thu, 19 Jan 2023 17:53:52 +0800 Subject: [PATCH 07/12] Wrap spinners to write logs, refactor secrets handling, set logs as global cli option --- packages/airnode-deployer/src/cli/commands.ts | 4 +- packages/airnode-deployer/src/cli/index.ts | 50 ++------- .../src/infrastructure/index.ts | 101 ++++++++++-------- packages/airnode-deployer/src/utils/logger.ts | 76 +++++++++---- 4 files changed, 125 insertions(+), 106 deletions(-) diff --git a/packages/airnode-deployer/src/cli/commands.ts b/packages/airnode-deployer/src/cli/commands.ts index 472c452f76..45394e6591 100644 --- a/packages/airnode-deployer/src/cli/commands.ts +++ b/packages/airnode-deployer/src/cli/commands.ts @@ -66,11 +66,11 @@ export async function deploy(configPath: string, secretsPath: string, receiptFil const output = goDeployAirnode.data; if (output.httpGatewayUrl) { logger.setSecret(output.httpGatewayUrl); - logger.info(`HTTP gateway URL: ${output.httpGatewayUrl}`); + logger.info(`HTTP gateway URL: ${output.httpGatewayUrl}`, { secrets: true }); } if (output.httpSignedDataGatewayUrl) { logger.setSecret(output.httpSignedDataGatewayUrl); - logger.info(`HTTP signed data gateway URL: ${output.httpSignedDataGatewayUrl}`); + logger.info(`HTTP signed data gateway URL: ${output.httpSignedDataGatewayUrl}`, { secrets: true }); } } diff --git a/packages/airnode-deployer/src/cli/index.ts b/packages/airnode-deployer/src/cli/index.ts index 14cd222ee6..a16e130332 100644 --- a/packages/airnode-deployer/src/cli/index.ts +++ b/packages/airnode-deployer/src/cli/index.ts @@ -30,7 +30,7 @@ function drawHeader() { async function runCommand(command: () => Promise) { const goCommand = await go(command); if (!goCommand.success) { - loggerUtils.log('\n\n\nError details:'); + logger.consoleLog('\n\n\nError details:'); // Logging an error here likely results in excessive logging since the errors are usually logged at the place where they // happen. However if we do not log the error here we risk having unhandled silent errors. The risk is not worth it. @@ -60,6 +60,12 @@ yargs(hideBin(process.argv)) default: false, type: 'boolean', }) + .option('logs', { + alias: 'l', + description: 'Output path for log files', + default: 'config/logs/', + type: 'string', + }) .command( 'deploy', 'Executes Airnode deployments specified in the config file', @@ -82,12 +88,6 @@ yargs(hideBin(process.argv)) default: 'config/receipt.json', type: 'string', }, - logs: { - alias: 'l', - description: 'Output path for log files', - default: 'config/logs/', - type: 'string', - }, // Flag arguments without value are not supported. See: https://github.com/yargs/yargs/issues/1532 'auto-remove': { description: 'Enable automatic removal of deployed resources for failed deployments', @@ -99,7 +99,7 @@ yargs(hideBin(process.argv)) drawHeader(); logger.debugMode(args.debug as boolean); - logger.setLogsDirectory(args.logs); + logger.setLogsDirectory(args.logs as string); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); await runCommand(() => deploy(args.configuration, args.secrets, args.receipt, args['auto-remove'])); } @@ -114,18 +114,12 @@ yargs(hideBin(process.argv)) default: 'config/receipt.json', type: 'string', }, - logs: { - alias: 'l', - description: 'Output path for log files', - default: 'config/logs/', - type: 'string', - }, }, async (args) => { drawHeader(); logger.debugMode(args.debug as boolean); - logger.setLogsDirectory(args.logs); + logger.setLogsDirectory(args.logs as string); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); await runCommand(() => removeWithReceipt(args.receipt)); @@ -140,12 +134,6 @@ yargs(hideBin(process.argv)) description: `ID of the deployment (from 'list' command)`, type: 'string', }); - yargs.option('logs', { - alias: 'l', - description: 'Output path for log files', - default: 'config/logs/', - type: 'string', - }); }, async (args) => { drawHeader(); @@ -173,16 +161,10 @@ yargs(hideBin(process.argv)) type: 'array', coerce: (option: CloudProvider['type'][]) => sortBy(uniq(option)), }, - logs: { - alias: 'l', - description: 'Output path for log files', - default: 'config/logs/', - type: 'string', - }, }, async (args) => { logger.debugMode(args.debug as boolean); - logger.setLogsDirectory(args.logs); + logger.setLogsDirectory(args.logs as string); logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`); await listAirnodes(args.cloudProviders); @@ -196,12 +178,6 @@ yargs(hideBin(process.argv)) description: `ID of the deployment (from 'list' command)`, type: 'string', }); - yargs.option('logs', { - alias: 'l', - description: 'Output path for log files', - default: 'config/logs/', - type: 'string', - }); }, async (args) => { logger.debugMode(args.debug as boolean); @@ -235,12 +211,6 @@ yargs(hideBin(process.argv)) default: 'config/', type: 'string', }); - yargs.option('logs', { - alias: 'l', - description: 'Output path for log files', - default: 'config/logs/', - type: 'string', - }); }, async (args) => { logger.debugMode(args.debug as boolean); diff --git a/packages/airnode-deployer/src/infrastructure/index.ts b/packages/airnode-deployer/src/infrastructure/index.ts index a2bb3fc094..f880bb02b1 100644 --- a/packages/airnode-deployer/src/infrastructure/index.ts +++ b/packages/airnode-deployer/src/infrastructure/index.ts @@ -15,7 +15,6 @@ import { deriveDeploymentId, deriveDeploymentVersionId, } from '@api3/airnode-node'; -import { consoleLog } from '@api3/airnode-utilities'; import { go, goSync } from '@api3/promise-utils'; import { unsafeParseConfigWithSecrets } from '@api3/airnode-validator'; import compact from 'lodash/compact'; @@ -69,25 +68,33 @@ interface CommandOptions extends child.ExecOptions { export async function runCommand(command: string, options: CommandOptions) { const stringifiedOptions = JSON.stringify(options); - const commandSpinner = logger.debugSpinner(`Running command '${command}' with options ${stringifiedOptions}`); + logger.useDebugSpinner(null, `Running command '${command}' with options ${stringifiedOptions}`, { + secrets: true, + }); const goExec = await go(() => exec(command, options)); if (!goExec.success) { if (options.ignoreError) { if (logger.inDebugMode()) { - logger.getSpinner().info(); + logger.useSpinner('info'); logger.warn(`Warning: ${goExec.error.message}`); } - commandSpinner.warn(`Command '${command}' with options ${stringifiedOptions} failed`); + logger.useDebugSpinner('warn', `Command '${command}' with options ${stringifiedOptions} failed`, { + secrets: true, + }); return ''; } - logger.getSpinner().info(); - commandSpinner.fail(`Command '${command}' with options ${stringifiedOptions} failed`); + logger.useSpinner('info'); + logger.useDebugSpinner('fail', `Command '${command}' with options ${stringifiedOptions} failed`, { + secrets: true, + }); throw logAndReturnError(goExec.error.toString()); } - commandSpinner.succeed(`Finished command '${command}' with options ${stringifiedOptions}`); + logger.useDebugSpinner('succeed', `Finished command '${command}' with options ${stringifiedOptions}`, { + secrets: true, + }); return goExec.data.stdout; } @@ -323,9 +330,9 @@ export const deployAirnode = async (config: Config, configPath: string, secretsP const airnodeAddress = deriveAirnodeAddress(airnodeWalletMnemonic); const { type, region } = cloudProvider as CloudProvider; - const spinner = logger.getSpinner().start(`Deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); + logger.useSpinner('start', `Deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); if (logger.inDebugMode()) { - spinner.info(); + logger.useSpinner('info'); } const goDeploy = await go(async () => { @@ -415,11 +422,11 @@ export const deployAirnode = async (config: Config, configPath: string, secretsP }); if (!goDeploy.success) { - spinner.fail(`Failed deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); + logger.useSpinner('fail', `Failed deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); throw goDeploy.error; } - spinner.succeed(`Deployed Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); + logger.useSpinner('succeed', `Deployed Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); return transformTerraformOutput(goDeploy.data); }; @@ -579,15 +586,15 @@ export async function removeAirnode(deploymentId: string) { throw new Error(`Invalid deployment ID '${deploymentId}'`); } - const spinner = logger.getSpinner().start(`Removing Airnode '${deploymentId}'`); + logger.useSpinner('start', `Removing Airnode '${deploymentId}'`); if (logger.inDebugMode()) { - spinner.info(); + logger.useSpinner('info'); } const goRemove = await go(async () => { const goCloudDeploymentInfo = await go(() => fetchDeployments(cloudProviderType, deploymentId)); if (!goCloudDeploymentInfo.success) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error( `Failed to fetch info about '${deploymentId}' from ${cloudProviderType.toUpperCase()}: ${ goCloudDeploymentInfo.error @@ -595,7 +602,7 @@ export async function removeAirnode(deploymentId: string) { ); } if (goCloudDeploymentInfo.data.length === 0) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error(`No deployment with ID '${deploymentId}' found`); } @@ -648,11 +655,11 @@ export async function removeAirnode(deploymentId: string) { }); if (!goRemove.success) { - spinner.fail(`Failed to remove Airnode '${deploymentId}'`); + logger.useSpinner('fail', `Failed to remove Airnode '${deploymentId}'`); throw goRemove.error; } - spinner.succeed(`Airnode '${deploymentId}' removed successfully`); + logger.useSpinner('succeed', `Airnode '${deploymentId}' removed successfully`); } export async function listAirnodes(cloudProviders: readonly CloudProvider['type'][]) { @@ -661,20 +668,22 @@ export async function listAirnodes(cloudProviders: readonly CloudProvider['type' for (const cloudProviderType of cloudProviders) { // Using different line of text for each cloud provider so we can easily convey which cloud provider failed // and which succeeded - const spinner = logger - .getSpinner() - .start(`Listing Airnode deployments from cloud provider ${cloudProviderType.toUpperCase()}`); + + logger.useSpinner('start', `Listing Airnode deployments from cloud provider ${cloudProviderType.toUpperCase()}`); if (logger.inDebugMode()) { - spinner.info(); + logger.useSpinner('info'); } const goListCloudAirnodes = await go(() => fetchDeployments(cloudProviderType)); if (goListCloudAirnodes.success) { - spinner.succeed(); + logger.useSpinner('succeed'); deployments.push(...goListCloudAirnodes.data); } else { - spinner.fail(`Failed to fetch deployments from ${cloudProviderType.toUpperCase()}: ${goListCloudAirnodes.error}`); + logger.useSpinner( + 'fail', + `Failed to fetch deployments from ${cloudProviderType.toUpperCase()}: ${goListCloudAirnodes.error}` + ); } } @@ -697,7 +706,7 @@ export async function listAirnodes(cloudProviders: readonly CloudProvider['type' ]) ); - consoleLog(table.toString()); + logger.consoleLog(table.toString()); } export async function deploymentInfo(deploymentId: string) { @@ -706,15 +715,15 @@ export async function deploymentInfo(deploymentId: string) { throw new Error(`Invalid deployment ID '${deploymentId}'`); } - const spinner = logger.getSpinner().start(`Fetching info about deployment '${deploymentId}'`); + logger.useSpinner('start', `Fetching info about deployment '${deploymentId}'`); if (logger.inDebugMode()) { - spinner.info(); + logger.useSpinner('info'); } const goCloudDeploymentInfo = await go(() => fetchDeployments(cloudProviderType, deploymentId)); if (!goCloudDeploymentInfo.success) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error( `Failed to fetch info about '${deploymentId}' from ${cloudProviderType.toUpperCase()}: ${ goCloudDeploymentInfo.error @@ -723,7 +732,7 @@ export async function deploymentInfo(deploymentId: string) { } if (goCloudDeploymentInfo.data.length === 0) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error(`No deployment with ID '${deploymentId}' found`); } @@ -739,15 +748,15 @@ export async function deploymentInfo(deploymentId: string) { }); table.push(...sortedVersions.map(({ id, timestamp }) => [id, timestampReadable(timestamp)])); - spinner.succeed(); - consoleLog(`Cloud provider: ${cloudProviderReadable(cloudProvider)}`); - consoleLog(`Airnode address: ${airnodeAddress}`); - consoleLog(`Stage: ${stage}`); - consoleLog(`Airnode version: ${airnodeVersion}`); - consoleLog(`Deployment ID: ${id}`); + logger.useSpinner('succeed'); + logger.consoleLog(`Cloud provider: ${cloudProviderReadable(cloudProvider)}`); + logger.consoleLog(`Airnode address: ${airnodeAddress}`); + logger.consoleLog(`Stage: ${stage}`); + logger.consoleLog(`Airnode version: ${airnodeVersion}`); + logger.consoleLog(`Deployment ID: ${id}`); const tableString = table.toString(); const tableStringWithCurrent = tableString.replace(new RegExp(`(?<=${currentVersionId}.*?)\n`), ' (current)\n'); - consoleLog(tableStringWithCurrent); + logger.consoleLog(tableStringWithCurrent); } export async function fetchFiles(deploymentId: string, outputDir: string, versionId?: string) { @@ -756,17 +765,19 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio throw new Error(`Invalid deployment ID '${deploymentId}'`); } - const spinner = logger - .getSpinner() - .start(`Fetching files for deployment '${deploymentId}'${versionId ? ` and version '${versionId}'` : ''}'`); + logger.useSpinner( + 'start', + `Fetching files for deployment '${deploymentId}'${versionId ? ` and version '${versionId}'` : ''}'` + ); + if (logger.inDebugMode()) { - spinner.info(); + logger.useSpinner('info'); } const goCloudDeploymentInfo = await go(() => fetchDeployments(cloudProviderType, deploymentId)); if (!goCloudDeploymentInfo.success) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error( `Failed to fetch info about '${deploymentId}' from ${cloudProviderType.toUpperCase()}: ${ goCloudDeploymentInfo.error @@ -775,7 +786,7 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio } if (goCloudDeploymentInfo.data.length === 0) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error(`No deployment with ID '${deploymentId}' found`); } @@ -785,7 +796,7 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio if (versionId) { requestedVersion = cloudDeploymentInfo.versions.find((version) => version.id === versionId); if (!requestedVersion) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error(`No deployment with ID '${deploymentId}' and version '${versionId}' found`); } } @@ -805,7 +816,7 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio const goOutputWritable = goSync(() => fs.accessSync(outputDir, fs.constants.W_OK)); if (!goOutputWritable.success) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error(`Can't write into an output directory '${outputDir}': ${goOutputWritable.error}`); } @@ -815,9 +826,9 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio const outputFile = path.join(outputDir, `${deploymentId}-${version.id}.zip`); const goWriteZip = await go(() => zip.writeZipPromise(outputFile)); if (!goWriteZip.success) { - spinner.stop(); + logger.useSpinner('stop'); throw new Error(`Can't create a zip file '${outputFile}': ${goWriteZip.error}`); } - spinner.succeed(`Files successfully downloaded as '${outputFile}'`); + logger.useSpinner('succeed', `Files successfully downloaded as '${outputFile}'`); } diff --git a/packages/airnode-deployer/src/utils/logger.ts b/packages/airnode-deployer/src/utils/logger.ts index a50492006a..6e1fcd47f3 100644 --- a/packages/airnode-deployer/src/utils/logger.ts +++ b/packages/airnode-deployer/src/utils/logger.ts @@ -1,13 +1,18 @@ import fs from 'fs'; +import path from 'path'; import * as ora from 'ora'; import { bold } from 'chalk'; import { format } from 'date-fns-tz'; +import { goSync } from '@api3/promise-utils'; +import { consoleLog as utilsConsoleLog } from '@api3/airnode-utilities'; +export type OraMethod = 'start' | 'succeed' | 'fail' | 'info' | 'stop' | 'warn'; export interface LoggerOptions { bold?: boolean; + secrets?: boolean; } -let logsDirectory = 'config/logs/'; +let logsDirectory: string; let logFileTimestamp: string; const secrets: string[] = []; @@ -34,28 +39,37 @@ export function getSpinner() { return spinner; } +export function useSpinner(method: OraMethod, text?: string, options?: LoggerOptions) { + if (text) writeLog(text, options); + getSpinner()[method](text); +} + function oraInstance(text?: string) { return debugModeFlag ? ora.default({ text, prefixText: () => new Date().toISOString() }) : ora.default(text); } -export function writeLog(text: string) { - const timestamp = format(Date.now(), 'yyyy-MM-dd HH:mm:ss'); - const sanitizedLogs = replaceSecrets(text, secrets); - fs.appendFileSync(`${logsDirectory}/deployer-${logFileTimestamp}.log`, `${timestamp}: ${sanitizedLogs}\n`); +export function writeLog(text: string, options?: LoggerOptions) { + if (!logsDirectory) throw new Error('Missing log file directory.'); + + const sanitizedLogs = options?.secrets ? replaceSecrets(text, secrets) : text; + fs.appendFileSync( + path.join(logsDirectory, `deployer-${logFileTimestamp}.log`), + `${new Date().toISOString()}: ${sanitizedLogs}\n` + ); } -export function succeed(text: string) { - writeLog(text); +export function succeed(text: string, options?: LoggerOptions) { + writeLog(text, options); oraInstance().succeed(text); } export function fail(text: string, options?: LoggerOptions) { - writeLog(text); + writeLog(text, options); oraInstance().fail(options?.bold ? bold(text) : text); } -export function warn(text: string) { - writeLog(text); +export function warn(text: string, options?: LoggerOptions) { + writeLog(text, options); const currentOra = getSpinner(); if (currentOra.isSpinning) { currentOra.clear(); @@ -64,8 +78,8 @@ export function warn(text: string) { oraInstance().warn(text); } -export function info(text: string) { - writeLog(text); +export function info(text: string, options?: LoggerOptions) { + writeLog(text, options); const currentOra = getSpinner(); if (currentOra.isSpinning) { currentOra.clear(); @@ -74,19 +88,38 @@ export function info(text: string) { oraInstance().info(text); } -export function debug(text: string) { - if (debugModeFlag) info(text); +export function debug(text: string, options?: LoggerOptions) { + if (debugModeFlag) { + info(text); + } else { + writeLog(text, options); + } } -export function debugSpinner(text: string) { - writeLog(text); +export function getDebugSpinner(text: string, options?: LoggerOptions) { + writeLog(text, options); return debugModeFlag ? getSpinner().info(text) : dummySpinner; } +export function useDebugSpinner(method: OraMethod | null, text?: string, options?: LoggerOptions) { + if (debugModeFlag) { + useSpinner(method || 'info', text, options); + return; + } + + if (text) writeLog(text, options); + if (method) dummySpinner[method](text); +} + export function debugMode(mode: boolean) { debugModeFlag = mode; } +export function consoleLog(text: string, options?: LoggerOptions) { + writeLog(text, options); + utilsConsoleLog(text); +} + export function inDebugMode() { return debugModeFlag; } @@ -97,14 +130,19 @@ export function setSecret(secret: string) { export function replaceSecrets(input: string, secrets: string[]) { let output = input; - secrets.forEach((secret) => (output = output.replace(secret, '*'.repeat(secret.length)))); + secrets.forEach((secret) => (output = output.replace(secret, '***'))); return output; } export function setLogsDirectory(path: string) { - logsDirectory = path.endsWith('/') ? path.slice(0, -1) : path; - if (!fs.existsSync(logsDirectory)) fs.mkdirSync(logsDirectory, { recursive: true }); + logsDirectory = path; + + const goOutputWritable = goSync(() => fs.accessSync(logsDirectory, fs.constants.W_OK)); + if (!goOutputWritable.success) { + const goMkdir = goSync(() => fs.mkdirSync(logsDirectory, { recursive: true })); + if (!goMkdir.success) throw new Error(`Failed to create logs output directory. Error: ${goMkdir.error}.`); + } logFileTimestamp = format(Date.now(), 'yyyy-MM-dd_HH:mm:ss'); } From b30a06a0aa250cc5434004014667de1a35174575 Mon Sep 17 00:00:00 2001 From: vponline Date: Thu, 19 Jan 2023 19:19:39 +0800 Subject: [PATCH 08/12] Fix writing tables to log files --- packages/airnode-deployer/src/utils/logger.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/airnode-deployer/src/utils/logger.ts b/packages/airnode-deployer/src/utils/logger.ts index 6e1fcd47f3..5ae4723b1e 100644 --- a/packages/airnode-deployer/src/utils/logger.ts +++ b/packages/airnode-deployer/src/utils/logger.ts @@ -6,6 +6,8 @@ import { format } from 'date-fns-tz'; import { goSync } from '@api3/promise-utils'; import { consoleLog as utilsConsoleLog } from '@api3/airnode-utilities'; +export const ANSI_REGEX = new RegExp(/\033\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]/g); + export type OraMethod = 'start' | 'succeed' | 'fail' | 'info' | 'stop' | 'warn'; export interface LoggerOptions { bold?: boolean; @@ -51,7 +53,9 @@ function oraInstance(text?: string) { export function writeLog(text: string, options?: LoggerOptions) { if (!logsDirectory) throw new Error('Missing log file directory.'); - const sanitizedLogs = options?.secrets ? replaceSecrets(text, secrets) : text; + const safeText = options?.secrets ? replaceSecrets(text, secrets) : text; + // Strip ANSI characters to write tables to log files correctly and add new line + const sanitizedLogs = ANSI_REGEX.test(safeText) ? '\n' + safeText.replace(ANSI_REGEX, '') : safeText; fs.appendFileSync( path.join(logsDirectory, `deployer-${logFileTimestamp}.log`), `${new Date().toISOString()}: ${sanitizedLogs}\n` From d0d99795b83cf19be85487623b1bec61e19425a0 Mon Sep 17 00:00:00 2001 From: vponline Date: Thu, 19 Jan 2023 20:32:57 +0800 Subject: [PATCH 09/12] Fix tests --- packages/airnode-deployer/src/cli/commands.test.ts | 9 +++++++-- packages/airnode-deployer/src/infrastructure/aws.test.ts | 5 +++++ packages/airnode-deployer/src/infrastructure/gcp.test.ts | 6 ++++++ .../airnode-deployer/src/infrastructure/index.test.ts | 4 +++- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/airnode-deployer/src/cli/commands.test.ts b/packages/airnode-deployer/src/cli/commands.test.ts index f90427d961..62da29eb38 100644 --- a/packages/airnode-deployer/src/cli/commands.test.ts +++ b/packages/airnode-deployer/src/cli/commands.test.ts @@ -1,13 +1,14 @@ +import fs from 'fs'; import { join } from 'path'; import { mockReadFileSync } from '../../test/mock-utils'; -import { readFileSync } from 'fs'; import { receipt } from '@api3/airnode-validator'; import { deploy, removeWithReceipt } from './commands'; import { version as packageVersion } from '../../package.json'; import * as logger from '../utils/logger'; import { removeAirnode } from '../infrastructure'; -const readExampleConfig = () => JSON.parse(readFileSync(join(__dirname, '../../config/config.example.json'), 'utf-8')); +const readExampleConfig = () => + JSON.parse(fs.readFileSync(join(__dirname, '../../config/config.example.json'), 'utf-8')); jest.mock('../infrastructure', () => ({ ...jest.requireActual('../infrastructure'), @@ -20,6 +21,10 @@ jest.mock('../utils', () => ({ writeReceiptFile: jest.fn(), })); +jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn()); +jest.spyOn(fs, 'mkdirSync').mockImplementation(); +logger.setLogsDirectory('/config/logs/'); + const gcpReceipt: receipt.Receipt = { airnodeWallet: { airnodeAddress: '0xF347ADEd76F7AC2013e379078738aBfF75780C2e', diff --git a/packages/airnode-deployer/src/infrastructure/aws.test.ts b/packages/airnode-deployer/src/infrastructure/aws.test.ts index 87ce46eca1..decf9d0e11 100644 --- a/packages/airnode-deployer/src/infrastructure/aws.test.ts +++ b/packages/airnode-deployer/src/infrastructure/aws.test.ts @@ -11,6 +11,7 @@ import { } from './aws'; import { mockBucketDirectoryStructure, mockBucketDirectoryStructureList } from '../../test/fixtures'; import { Directory } from '../utils/infrastructure'; +import { setLogsDirectory } from '../utils/logger'; const mockPromise = (fn: Function) => () => ({ promise: fn }); @@ -53,6 +54,10 @@ const awsDeleteObjectsSpy: jest.SpyInstance = jest.requireMock('aws-sdk').S3().d const awsDeleteBucketSpy: jest.SpyInstance = jest.requireMock('aws-sdk').S3().deleteBucket; const generateBucketNameSpy: jest.SpyInstance = jest.requireMock('../utils/infrastructure').generateBucketName; +jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn()); +jest.spyOn(fs, 'mkdirSync').mockImplementation(); +setLogsDirectory('/config/logs/'); + const cloudProvider = { type: 'aws' as const, region: 'us-east-1', diff --git a/packages/airnode-deployer/src/infrastructure/gcp.test.ts b/packages/airnode-deployer/src/infrastructure/gcp.test.ts index 3fc0f0b5c4..836678fb7a 100644 --- a/packages/airnode-deployer/src/infrastructure/gcp.test.ts +++ b/packages/airnode-deployer/src/infrastructure/gcp.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { copyFileInBucket, createAirnodeBucket, @@ -10,6 +11,7 @@ import { } from './gcp'; import { mockBucketDirectoryStructure, mockBucketDirectoryStructureList } from '../../test/fixtures'; import { Directory } from '../utils/infrastructure'; +import { setLogsDirectory } from '../utils/logger'; const bucketName = 'airnode-aabbccdd0011'; @@ -63,6 +65,10 @@ const gcsCopySpy: jest.SpyInstance = jest.requireMock('@google-cloud/storage').S const gcsFileDeleteSpy: jest.SpyInstance = jest.requireMock('@google-cloud/storage').Storage().bucket().file().delete; const generateBucketNameSpy: jest.SpyInstance = jest.requireMock('../utils/infrastructure').generateBucketName; +jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn()); +jest.spyOn(fs, 'mkdirSync').mockImplementation(); +setLogsDirectory('/config/logs/'); + const cloudProvider = { type: 'gcp' as const, region: 'us-east1', diff --git a/packages/airnode-deployer/src/infrastructure/index.test.ts b/packages/airnode-deployer/src/infrastructure/index.test.ts index 9aa76efb08..e7f51ab2b9 100644 --- a/packages/airnode-deployer/src/infrastructure/index.test.ts +++ b/packages/airnode-deployer/src/infrastructure/index.test.ts @@ -8,7 +8,7 @@ import AdmZip from 'adm-zip'; import { AwsCloudProvider, GcpCloudProvider, loadTrustedConfig } from '@api3/airnode-node'; import * as aws from './aws'; import * as gcp from './gcp'; -import { getSpinner } from '../utils/logger'; +import { getSpinner, setLogsDirectory } from '../utils/logger'; import { parseSecretsFile } from '../utils'; import { Directory, DirectoryStructure } from '../utils/infrastructure'; import { mockBucketDirectoryStructure } from '../../test/fixtures'; @@ -21,6 +21,8 @@ jest.mock('../../package.json', () => ({ const exec = jest.fn(); jest.spyOn(util, 'promisify').mockImplementation(() => exec); jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn()); +jest.spyOn(fs, 'mkdirSync').mockImplementation(); +setLogsDirectory('/config/logs/'); import { version as nodeVersion } from '../../package.json'; import * as infrastructure from '.'; From 5054a2b03630647466edb80c9226e0368b3b77de Mon Sep 17 00:00:00 2001 From: vponline Date: Mon, 23 Jan 2023 19:16:03 +0800 Subject: [PATCH 10/12] Refactor spinner wrapper --- .../src/infrastructure/index.ts | 86 ++++++++----------- packages/airnode-deployer/src/utils/logger.ts | 75 ++++++++++++---- 2 files changed, 95 insertions(+), 66 deletions(-) diff --git a/packages/airnode-deployer/src/infrastructure/index.ts b/packages/airnode-deployer/src/infrastructure/index.ts index f880bb02b1..706c189a85 100644 --- a/packages/airnode-deployer/src/infrastructure/index.ts +++ b/packages/airnode-deployer/src/infrastructure/index.ts @@ -68,33 +68,25 @@ interface CommandOptions extends child.ExecOptions { export async function runCommand(command: string, options: CommandOptions) { const stringifiedOptions = JSON.stringify(options); - logger.useDebugSpinner(null, `Running command '${command}' with options ${stringifiedOptions}`, { - secrets: true, - }); + const commandSpinner = logger.debugSpinner(`Running command '${command}' with options ${stringifiedOptions}`); const goExec = await go(() => exec(command, options)); if (!goExec.success) { if (options.ignoreError) { if (logger.inDebugMode()) { - logger.useSpinner('info'); + logger.getSpinner().info(); logger.warn(`Warning: ${goExec.error.message}`); } - logger.useDebugSpinner('warn', `Command '${command}' with options ${stringifiedOptions} failed`, { - secrets: true, - }); + commandSpinner.warn(`Command '${command}' with options ${stringifiedOptions} failed`); return ''; } - logger.useSpinner('info'); - logger.useDebugSpinner('fail', `Command '${command}' with options ${stringifiedOptions} failed`, { - secrets: true, - }); + logger.getSpinner().info(); + commandSpinner.fail(`Command '${command}' with options ${stringifiedOptions} failed`); throw logAndReturnError(goExec.error.toString()); } - logger.useDebugSpinner('succeed', `Finished command '${command}' with options ${stringifiedOptions}`, { - secrets: true, - }); + commandSpinner.succeed(`Finished command '${command}' with options ${stringifiedOptions}`); return goExec.data.stdout; } @@ -330,9 +322,10 @@ export const deployAirnode = async (config: Config, configPath: string, secretsP const airnodeAddress = deriveAirnodeAddress(airnodeWalletMnemonic); const { type, region } = cloudProvider as CloudProvider; - logger.useSpinner('start', `Deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); + const spinner = logger.getSpinner(); + spinner.start(`Deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); if (logger.inDebugMode()) { - logger.useSpinner('info'); + spinner.info(); } const goDeploy = await go(async () => { @@ -422,11 +415,11 @@ export const deployAirnode = async (config: Config, configPath: string, secretsP }); if (!goDeploy.success) { - logger.useSpinner('fail', `Failed deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); + spinner.fail(`Failed deploying Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); throw goDeploy.error; } - logger.useSpinner('succeed', `Deployed Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); + spinner.succeed(`Deployed Airnode ${airnodeAddress} ${stage} to ${type} ${region}`); return transformTerraformOutput(goDeploy.data); }; @@ -586,15 +579,16 @@ export async function removeAirnode(deploymentId: string) { throw new Error(`Invalid deployment ID '${deploymentId}'`); } - logger.useSpinner('start', `Removing Airnode '${deploymentId}'`); + const spinner = logger.getSpinner(); + spinner.start(`Removing Airnode '${deploymentId}'`); if (logger.inDebugMode()) { - logger.useSpinner('info'); + spinner.info(); } const goRemove = await go(async () => { const goCloudDeploymentInfo = await go(() => fetchDeployments(cloudProviderType, deploymentId)); if (!goCloudDeploymentInfo.success) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error( `Failed to fetch info about '${deploymentId}' from ${cloudProviderType.toUpperCase()}: ${ goCloudDeploymentInfo.error @@ -602,7 +596,7 @@ export async function removeAirnode(deploymentId: string) { ); } if (goCloudDeploymentInfo.data.length === 0) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error(`No deployment with ID '${deploymentId}' found`); } @@ -655,11 +649,11 @@ export async function removeAirnode(deploymentId: string) { }); if (!goRemove.success) { - logger.useSpinner('fail', `Failed to remove Airnode '${deploymentId}'`); + spinner.fail(`Failed to remove Airnode '${deploymentId}'`); throw goRemove.error; } - logger.useSpinner('succeed', `Airnode '${deploymentId}' removed successfully`); + spinner.succeed(`Airnode '${deploymentId}' removed successfully`); } export async function listAirnodes(cloudProviders: readonly CloudProvider['type'][]) { @@ -668,22 +662,19 @@ export async function listAirnodes(cloudProviders: readonly CloudProvider['type' for (const cloudProviderType of cloudProviders) { // Using different line of text for each cloud provider so we can easily convey which cloud provider failed // and which succeeded - - logger.useSpinner('start', `Listing Airnode deployments from cloud provider ${cloudProviderType.toUpperCase()}`); + const spinner = logger.getSpinner(); + spinner.start(`Listing Airnode deployments from cloud provider ${cloudProviderType.toUpperCase()}`); if (logger.inDebugMode()) { - logger.useSpinner('info'); + spinner.info(); } const goListCloudAirnodes = await go(() => fetchDeployments(cloudProviderType)); if (goListCloudAirnodes.success) { - logger.useSpinner('succeed'); + spinner.succeed(); deployments.push(...goListCloudAirnodes.data); } else { - logger.useSpinner( - 'fail', - `Failed to fetch deployments from ${cloudProviderType.toUpperCase()}: ${goListCloudAirnodes.error}` - ); + spinner.fail(`Failed to fetch deployments from ${cloudProviderType.toUpperCase()}: ${goListCloudAirnodes.error}`); } } @@ -715,15 +706,16 @@ export async function deploymentInfo(deploymentId: string) { throw new Error(`Invalid deployment ID '${deploymentId}'`); } - logger.useSpinner('start', `Fetching info about deployment '${deploymentId}'`); + const spinner = logger.getSpinner(); + spinner.start(`Fetching info about deployment '${deploymentId}'`); if (logger.inDebugMode()) { - logger.useSpinner('info'); + spinner.info(); } const goCloudDeploymentInfo = await go(() => fetchDeployments(cloudProviderType, deploymentId)); if (!goCloudDeploymentInfo.success) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error( `Failed to fetch info about '${deploymentId}' from ${cloudProviderType.toUpperCase()}: ${ goCloudDeploymentInfo.error @@ -732,7 +724,7 @@ export async function deploymentInfo(deploymentId: string) { } if (goCloudDeploymentInfo.data.length === 0) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error(`No deployment with ID '${deploymentId}' found`); } @@ -748,7 +740,7 @@ export async function deploymentInfo(deploymentId: string) { }); table.push(...sortedVersions.map(({ id, timestamp }) => [id, timestampReadable(timestamp)])); - logger.useSpinner('succeed'); + spinner.succeed(); logger.consoleLog(`Cloud provider: ${cloudProviderReadable(cloudProvider)}`); logger.consoleLog(`Airnode address: ${airnodeAddress}`); logger.consoleLog(`Stage: ${stage}`); @@ -765,19 +757,17 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio throw new Error(`Invalid deployment ID '${deploymentId}'`); } - logger.useSpinner( - 'start', - `Fetching files for deployment '${deploymentId}'${versionId ? ` and version '${versionId}'` : ''}'` - ); + const spinner = logger.getSpinner(); + spinner.start(`Fetching files for deployment '${deploymentId}'${versionId ? ` and version '${versionId}'` : ''}'`); if (logger.inDebugMode()) { - logger.useSpinner('info'); + spinner.info(); } const goCloudDeploymentInfo = await go(() => fetchDeployments(cloudProviderType, deploymentId)); if (!goCloudDeploymentInfo.success) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error( `Failed to fetch info about '${deploymentId}' from ${cloudProviderType.toUpperCase()}: ${ goCloudDeploymentInfo.error @@ -786,7 +776,7 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio } if (goCloudDeploymentInfo.data.length === 0) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error(`No deployment with ID '${deploymentId}' found`); } @@ -796,7 +786,7 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio if (versionId) { requestedVersion = cloudDeploymentInfo.versions.find((version) => version.id === versionId); if (!requestedVersion) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error(`No deployment with ID '${deploymentId}' and version '${versionId}' found`); } } @@ -816,7 +806,7 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio const goOutputWritable = goSync(() => fs.accessSync(outputDir, fs.constants.W_OK)); if (!goOutputWritable.success) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error(`Can't write into an output directory '${outputDir}': ${goOutputWritable.error}`); } @@ -826,9 +816,9 @@ export async function fetchFiles(deploymentId: string, outputDir: string, versio const outputFile = path.join(outputDir, `${deploymentId}-${version.id}.zip`); const goWriteZip = await go(() => zip.writeZipPromise(outputFile)); if (!goWriteZip.success) { - logger.useSpinner('stop'); + spinner.stop(); throw new Error(`Can't create a zip file '${outputFile}': ${goWriteZip.error}`); } - logger.useSpinner('succeed', `Files successfully downloaded as '${outputFile}'`); + spinner.succeed(`Files successfully downloaded as '${outputFile}'`); } diff --git a/packages/airnode-deployer/src/utils/logger.ts b/packages/airnode-deployer/src/utils/logger.ts index 5ae4723b1e..563f5eb071 100644 --- a/packages/airnode-deployer/src/utils/logger.ts +++ b/packages/airnode-deployer/src/utils/logger.ts @@ -8,12 +8,59 @@ import { consoleLog as utilsConsoleLog } from '@api3/airnode-utilities'; export const ANSI_REGEX = new RegExp(/\033\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]/g); -export type OraMethod = 'start' | 'succeed' | 'fail' | 'info' | 'stop' | 'warn'; export interface LoggerOptions { bold?: boolean; secrets?: boolean; } +class Spinner { + private oraInstance; + + constructor() { + this.oraInstance = oraInstance(); + } + + isSpinning() { + return this.oraInstance.isSpinning; + } + + clear() { + this.oraInstance.clear(); + } + frame() { + this.oraInstance.frame(); + } + + private getOraInstance() { + return this.oraInstance; + } + + start(text?: string, options?: LoggerOptions) { + if (text) writeLog(text, options); + this.getOraInstance().start(text); + } + succeed(text?: string, options?: LoggerOptions) { + if (text) writeLog(text, options); + this.getOraInstance().succeed(text); + } + fail(text?: string, options?: LoggerOptions) { + if (text) writeLog(text, options); + this.getOraInstance().fail(text); + } + info(text?: string, options?: LoggerOptions) { + if (text) writeLog(text, options); + this.getOraInstance().info(text); + } + warn(text?: string, options?: LoggerOptions) { + if (text) writeLog(text, options); + this.getOraInstance().warn(text); + } + stop(text?: string, options?: LoggerOptions) { + if (text) writeLog(text, options); + this.getOraInstance().stop(); + } +} + let logsDirectory: string; let logFileTimestamp: string; const secrets: string[] = []; @@ -32,20 +79,15 @@ const dummySpinner: ora.Ora = { clear: () => dummySpinner, render: () => dummySpinner, }; -let spinner: ora.Ora; +let spinner: Spinner; export function getSpinner() { if (spinner) return spinner; - spinner = oraInstance(); + spinner = new Spinner(); return spinner; } -export function useSpinner(method: OraMethod, text?: string, options?: LoggerOptions) { - if (text) writeLog(text, options); - getSpinner()[method](text); -} - function oraInstance(text?: string) { return debugModeFlag ? ora.default({ text, prefixText: () => new Date().toISOString() }) : ora.default(text); } @@ -75,7 +117,7 @@ export function fail(text: string, options?: LoggerOptions) { export function warn(text: string, options?: LoggerOptions) { writeLog(text, options); const currentOra = getSpinner(); - if (currentOra.isSpinning) { + if (currentOra.isSpinning()) { currentOra.clear(); currentOra.frame(); } @@ -85,7 +127,7 @@ export function warn(text: string, options?: LoggerOptions) { export function info(text: string, options?: LoggerOptions) { writeLog(text, options); const currentOra = getSpinner(); - if (currentOra.isSpinning) { + if (currentOra.isSpinning()) { currentOra.clear(); currentOra.frame(); } @@ -100,19 +142,16 @@ export function debug(text: string, options?: LoggerOptions) { } } -export function getDebugSpinner(text: string, options?: LoggerOptions) { +export function debugSpinner(text: string, options?: LoggerOptions) { writeLog(text, options); - return debugModeFlag ? getSpinner().info(text) : dummySpinner; -} -export function useDebugSpinner(method: OraMethod | null, text?: string, options?: LoggerOptions) { if (debugModeFlag) { - useSpinner(method || 'info', text, options); - return; + const spinner = getSpinner(); + spinner.info(text); + return spinner; } - if (text) writeLog(text, options); - if (method) dummySpinner[method](text); + return dummySpinner; } export function debugMode(mode: boolean) { From cca40b9a72063f4e3a7bde991c2abebef0aa278f Mon Sep 17 00:00:00 2001 From: vponline Date: Tue, 24 Jan 2023 00:31:44 +0800 Subject: [PATCH 11/12] Fix getSpinner mock, formatting --- packages/airnode-deployer/src/cli/commands.test.ts | 4 +++- packages/airnode-deployer/src/utils/logger.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/airnode-deployer/src/cli/commands.test.ts b/packages/airnode-deployer/src/cli/commands.test.ts index 4f200dee5b..45bfbbc55f 100644 --- a/packages/airnode-deployer/src/cli/commands.test.ts +++ b/packages/airnode-deployer/src/cli/commands.test.ts @@ -70,7 +70,9 @@ describe('deployer commands', () => { loggerSucceedSpy = jest.spyOn(logger, 'succeed').mockImplementation(() => {}); jest .spyOn(logger, 'getSpinner') - .mockImplementation(() => ({ start: () => mockSpinner } as unknown as logger.Spinner)); + .mockImplementation( + () => ({ start: () => mockSpinner, succeed: () => mockSpinner } as unknown as logger.Spinner) + ); jest.spyOn(logger, 'inDebugMode').mockImplementation(() => false); tempConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'airnode-rollback-test')); fs.copyFileSync(path.join(__dirname, '../../config/config.example.json'), path.join(tempConfigDir, 'config.json')); diff --git a/packages/airnode-deployer/src/utils/logger.ts b/packages/airnode-deployer/src/utils/logger.ts index ab2d1fd952..e3b6997af5 100644 --- a/packages/airnode-deployer/src/utils/logger.ts +++ b/packages/airnode-deployer/src/utils/logger.ts @@ -27,6 +27,7 @@ export class Spinner { clear() { this.oraInstance.clear(); } + frame() { this.oraInstance.frame(); } @@ -39,22 +40,27 @@ export class Spinner { if (text) writeLog(text, options); this.getOraInstance().start(text); } + succeed(text?: string, options?: LoggerOptions) { if (text) writeLog(text, options); this.getOraInstance().succeed(text); } + fail(text?: string, options?: LoggerOptions) { if (text) writeLog(text, options); this.getOraInstance().fail(text); } + info(text?: string, options?: LoggerOptions) { if (text) writeLog(text, options); this.getOraInstance().info(text); } + warn(text?: string, options?: LoggerOptions) { if (text) writeLog(text, options); this.getOraInstance().warn(text); } + stop(text?: string, options?: LoggerOptions) { if (text) writeLog(text, options); this.getOraInstance().stop(); From 681d6280a25cab341b3174cdfb63cf340f4731f0 Mon Sep 17 00:00:00 2001 From: vponline Date: Tue, 24 Jan 2023 14:44:53 +0800 Subject: [PATCH 12/12] Refactor Spinner class --- packages/airnode-deployer/src/utils/logger.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/airnode-deployer/src/utils/logger.ts b/packages/airnode-deployer/src/utils/logger.ts index e3b6997af5..e3143a4cb9 100644 --- a/packages/airnode-deployer/src/utils/logger.ts +++ b/packages/airnode-deployer/src/utils/logger.ts @@ -20,20 +20,20 @@ export class Spinner { this.oraInstance = oraInstance(); } + private getOraInstance() { + return this.oraInstance; + } + isSpinning() { - return this.oraInstance.isSpinning; + return this.getOraInstance().isSpinning; } clear() { - this.oraInstance.clear(); + this.getOraInstance().clear(); } frame() { - this.oraInstance.frame(); - } - - private getOraInstance() { - return this.oraInstance; + this.getOraInstance().frame(); } start(text?: string, options?: LoggerOptions) {