Skip to content

Commit

Permalink
Deployer logs file (#1611)
Browse files Browse the repository at this point in the history
* Write deployer logs and errors to file, throw general error for terraform command failures

* Add changeset

* Refactor logs creation, add cli option for logs directory, hide secrets

* Mock writeLog in tests

* Replace mocking writeLog with fs.appendFileSync

* Add trailing slash to logs default dir for consistency

* Wrap spinners to write logs, refactor secrets handling, set logs as global cli option

* Fix writing tables to log files

* Fix tests

* Refactor spinner wrapper

* Fix getSpinner mock, formatting

* Refactor Spinner class
  • Loading branch information
vponline authored Jan 26, 2023
1 parent 588b1be commit 2a66b8d
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 67 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-beds-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@api3/airnode-deployer': patch
---

Write deployer logs to file
3 changes: 3 additions & 0 deletions packages/airnode-deployer/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ coverage/
.terraform*
terraform.tfstate*
*.tfvars

# Logs
/logs
10 changes: 8 additions & 2 deletions packages/airnode-deployer/src/cli/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import os from 'os';
import path from 'path';
import { mockReadFileSync } from '../../test/mock-utils';
import { receipt } from '@api3/airnode-validator';
import { Ora } from 'ora';
import { deploy, removeWithReceipt, rollback } from './commands';
import { version as packageVersion } from '../../package.json';
import * as logger from '../utils/logger';
Expand All @@ -24,6 +23,9 @@ jest.mock('../utils', () => ({
writeReceiptFile: jest.fn(),
}));

jest.spyOn(fs, 'appendFileSync').mockImplementation(() => jest.fn());
jest.spyOn(fs, 'mkdirSync').mockImplementation();
logger.setLogsDirectory('/config/logs/');
const mockSpinner = {
stop: jest.fn(),
succeed: jest.fn(),
Expand Down Expand Up @@ -66,7 +68,11 @@ describe('deployer commands', () => {
mockWriteReceiptFile = jest.requireMock('../utils').writeReceiptFile;
loggerFailSpy = jest.spyOn(logger, 'fail').mockImplementation(() => {});
loggerSucceedSpy = jest.spyOn(logger, 'succeed').mockImplementation(() => {});
jest.spyOn(logger, 'getSpinner').mockImplementation(() => ({ start: () => mockSpinner } as unknown as Ora));
jest
.spyOn(logger, 'getSpinner')
.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'));
Expand Down
45 changes: 24 additions & 21 deletions packages/airnode-deployer/src/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as os from 'os';
import * as path from 'path';
import { loadConfig, deriveDeploymentVersionId } from '@api3/airnode-node';
import { go } from '@api3/promise-utils';
import { bold } from 'chalk';
import { deployAirnode, removeAirnode, saveDeploymentFiles } from '../infrastructure';
import { writeReceiptFile, parseReceiptFile, parseSecretsFile, deriveAirnodeAddress } from '../utils';
import * as logger from '../utils/logger';
Expand All @@ -28,36 +27,33 @@ 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;
}

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([
Expand All @@ -71,8 +67,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}`, { secrets: true });
}
if (output.httpSignedDataGatewayUrl) {
logger.setSecret(output.httpSignedDataGatewayUrl);
logger.info(`HTTP signed data gateway URL: ${output.httpSignedDataGatewayUrl}`, { secrets: true });
}

return { creationTime: time };
}
Expand All @@ -86,7 +88,8 @@ export async function removeWithReceipt(receiptFilename: string) {
}

export async function rollback(deploymentId: string, versionId: string, receiptFile: string, autoRemove: boolean) {
const spinner = logger.getSpinner().start(`Rollback of deployment '${deploymentId}' to version '${versionId}'`);
const spinner = logger.getSpinner();
spinner.start(`Rollback of deployment '${deploymentId}' to version '${versionId}'`);
if (logger.inDebugMode()) {
spinner.info();
}
Expand Down
16 changes: 14 additions & 2 deletions packages/airnode-deployer/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function drawHeader() {
async function runCommand(command: () => Promise<any>) {
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.
Expand All @@ -46,7 +46,7 @@ async function runCommand(command: () => Promise<any>) {
}

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',
Expand All @@ -61,6 +61,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',
Expand Down Expand Up @@ -94,6 +100,7 @@ yargs(hideBin(process.argv))
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(() => deploy(args.configuration, args.secrets, args.receipt, args['auto-remove']));
}
Expand All @@ -113,6 +120,7 @@ yargs(hideBin(process.argv))
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(() => removeWithReceipt(args.receipt));
Expand All @@ -132,6 +140,7 @@ yargs(hideBin(process.argv))
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(() =>
Expand All @@ -156,6 +165,7 @@ yargs(hideBin(process.argv))
},
async (args) => {
logger.debugMode(args.debug as boolean);
logger.setLogsDirectory(args.logs as string);
logger.debug(`Running command ${args._[0]} with arguments ${longArguments(args)}`);

await listAirnodes(args.cloudProviders);
Expand All @@ -172,6 +182,7 @@ yargs(hideBin(process.argv))
},
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
Expand Down Expand Up @@ -204,6 +215,7 @@ yargs(hideBin(process.argv))
},
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
Expand Down
5 changes: 5 additions & 0 deletions packages/airnode-deployer/src/infrastructure/aws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions packages/airnode-deployer/src/infrastructure/gcp.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from 'fs';
import {
copyFileInBucket,
createAirnodeBucket,
Expand All @@ -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';

Expand Down Expand Up @@ -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',
Expand Down
19 changes: 10 additions & 9 deletions packages/airnode-deployer/src/infrastructure/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,6 +20,9 @@ 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 '.';
Expand Down Expand Up @@ -810,11 +813,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 files for more details.'
);
});
});
Expand Down Expand Up @@ -1131,10 +1132,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 files for more details.'
);
});
});

Expand Down
Loading

0 comments on commit 2a66b8d

Please sign in to comment.