Skip to content

Commit

Permalink
Merge pull request #490 from HubSpot/add/app-function-logs
Browse files Browse the repository at this point in the history
 Add ability to retrieve and tail app function logs
  • Loading branch information
gcorne authored May 7, 2021
2 parents 0c1bfa1 + d86efc7 commit 93d2d15
Show file tree
Hide file tree
Showing 10 changed files with 1,279 additions and 946 deletions.
3 changes: 0 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// Chalk can cause snapshots to with its styling, just disable the color instead
process.env.FORCE_COLOR = 0;

module.exports = {
testEnvironment: 'node',
projects: ['<rootDir>/packages/*'],
Expand Down
25 changes: 24 additions & 1 deletion packages/cli-lib/__tests__/__snapshots__/schema.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`cli-lib/schema cleanSchema() cleans a basic schema 1`] = `
Object {
"associatedObjects": null,
"labels": Object {
"plural": "Schemas",
"singular": "Schema",
},
"name": "schema",
"primaryDisplayProperty": "name",
"properties": Array [
Object {
"description": "Name Field",
"fieldType": "text",
"label": "Name",
"name": "name",
"type": "string",
},
],
"requiredProperties": Array [],
"searchableProperties": Array [],
}
`;

exports[`cli-lib/schema cleanSchema() cleans a full schema 1`] = `
Object {
"associatedObjects": undefined,
Expand Down Expand Up @@ -176,7 +199,7 @@ Array [
exports[`cli-lib/schema logSchemas() logs schemas 1`] = `
"╔════════╤════════╤══════════════╗
║ Label │ Name │ objectTypeId ║
║ Schema │ schema │
║ Schema │ schema │ 2-123
╚════════╧════════╧══════════════╝
"
`;
Expand Down
1 change: 1 addition & 0 deletions packages/cli-lib/__tests__/fixtures/schema/basic.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"requiredProperties": [],
"searchableProperties": [],
"primaryDisplayProperty": "name",
"objectTypeId": "2-123",
"properties": [
{
"name": "name",
Expand Down
10 changes: 9 additions & 1 deletion packages/cli-lib/__tests__/schema.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const { cleanSchema, writeSchemaToDisk, logSchemas } = require('../schema');
const { logger } = require('../logger');
const { getCwd } = require('../path');
Expand All @@ -8,9 +9,16 @@ const full = require('./fixtures/schema/full.json');
const multiple = require('./fixtures/schema/multiple.json');

describe('cli-lib/schema', () => {
const originalChalkLevel = chalk.level;
beforeEach(() => {
chalk.level = 0;
});
afterEach(() => {
chalk.level = originalChalkLevel;
});
describe('cleanSchema()', () => {
it('cleans a basic schema', () => {
expect(cleanSchema(basic)).toEqual(basic);
expect(cleanSchema(basic)).toMatchSnapshot();
});

it('cleans a full schema', () => {
Expand Down
23 changes: 23 additions & 0 deletions packages/cli-lib/api/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,30 @@ async function getBuildStatus(portalId, buildId) {
});
}

async function getAppFunctionLogs(
accountId,
functionName,
appPath,
query = {}
) {
const { limit = 5 } = query;

return http.get(accountId, {
uri: `${FUNCTION_API_PATH}/app-function/logs/${functionName}`,
query: { ...query, limit, appPath },
});
}

async function getLatestAppFunctionLogs(accountId, functionName, appPath) {
return http.get(accountId, {
uri: `${FUNCTION_API_PATH}/app-function/logs/${functionName}/latest`,
query: { appPath },
});
}

module.exports = {
getAppFunctionLogs,
getLatestAppFunctionLogs,
buildPackage,
getBuildStatus,
getFunctionByPath,
Expand Down
172 changes: 81 additions & 91 deletions packages/cli/commands/logs.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const readline = require('readline');
const ora = require('ora');
const {
addAccountOptions,
Expand All @@ -20,38 +19,17 @@ const {
ApiErrorContext,
} = require('@hubspot/cli-lib/errorHandlers');
const { outputLogs } = require('@hubspot/cli-lib/lib/logs');
const { getFunctionByPath } = require('@hubspot/cli-lib/api/functions');
const {
getFunctionByPath,
getAppFunctionLogs,
getLatestAppFunctionLogs,
} = require('@hubspot/cli-lib/api/functions');
const {
getFunctionLogs,
getLatestFunctionLog,
} = require('@hubspot/cli-lib/api/results');
const { base64EncodeString } = require('@hubspot/cli-lib/lib/encoding');
const { validateAccount } = require('../lib/validation');

const TAIL_DELAY = 5000;

const makeSpinner = (functionPath, accountId) => {
return ora(
`Waiting for log entries for '${functionPath}' on account '${accountId}'.\n`
);
};

const makeTailCall = (accountId, functionId) => {
return async after => {
const latestLog = await getFunctionLogs(accountId, functionId, { after });
return latestLog;
};
};

const handleKeypressToExit = exit => {
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.on('keypress', (str, key) => {
if (key && ((key.ctrl && key.name == 'c') || key.name === 'escape')) {
exit();
}
});
};
const { tailLogs } = require('../lib/serverlessLogs');

const loadAndValidateOptions = async options => {
setLogLevel(options);
Expand All @@ -65,66 +43,8 @@ const loadAndValidateOptions = async options => {
}
};

const tailLogs = async ({
functionId,
functionPath,
accountId,
accountName,
compact,
}) => {
const tailCall = makeTailCall(accountId, functionId);
const spinner = makeSpinner(functionPath, accountName || accountId);
let initialAfter;

spinner.start();

try {
const latestLog = await getLatestFunctionLog(accountId, functionId);
initialAfter = base64EncodeString(latestLog.id);
} catch (e) {
// A 404 means no latest log exists(never executed)
if (e.statusCode !== 404) {
await logServerlessFunctionApiErrorInstance(
accountId,
e,
new ApiErrorContext({ accountId, functionPath })
);
}
}

const tail = async after => {
const latestLog = await tailCall(after);

if (latestLog.results.length) {
spinner.clear();
outputLogs(latestLog, {
compact,
});
}

setTimeout(() => {
tail(latestLog.paging.next.after);
}, TAIL_DELAY);
};

handleKeypressToExit(() => {
spinner.stop();
process.exit();
});
tail(initialAfter);
};

exports.command = 'logs <endpoint>';
exports.describe = 'get logs for a function';

exports.handler = async options => {
loadAndValidateOptions(options);

const endpointLog = async (accountId, options) => {
const { latest, follow, compact, endpoint: functionPath } = options;
let logsResp;
const accountId = getAccountId(options);

trackCommandUsage('logs', { latest }, accountId);

logger.debug(
`Getting ${
Expand All @@ -142,16 +62,24 @@ exports.handler = async options => {
process.exit();
}
);
const functionId = functionResp.id;

logger.debug(`Retrieving logs for functionId: ${functionResp.id}`);

let logsResp;

if (follow) {
const spinner = ora(
`Waiting for log entries for '${functionPath}' on account '${accountId}'.\n`
);
const tailCall = after => getFunctionLogs(accountId, functionId, { after });
const fetchLatest = () => getLatestFunctionLog(accountId, functionId);
await tailLogs({
functionId: functionResp.id,
functionPath,
accountId,
accountName: options.portal,
compact,
spinner,
tailCall,
fetchLatest,
});
} else if (latest) {
logsResp = await getLatestFunctionLog(accountId, functionResp.id);
Expand All @@ -164,13 +92,74 @@ exports.handler = async options => {
}
};

const appFunctionLog = async (accountId, options) => {
const { latest, follow, compact, functionName, appPath } = options;

let logsResp;

if (follow) {
const spinner = ora(
`Waiting for log entries for "${functionName}" on account "${accountId}".\n`
);
const tailCall = after =>
getAppFunctionLogs(accountId, functionName, appPath, { after });
const fetchLatest = () =>
getLatestAppFunctionLogs(accountId, functionName, appPath);

await tailLogs({
accountId,
compact,
spinner,
tailCall,
fetchLatest,
});
} else if (latest) {
logsResp = await getLatestAppFunctionLogs(accountId, functionName, appPath);
} else {
logsResp = await getAppFunctionLogs(accountId, functionName, appPath, {});
}

if (logsResp) {
return outputLogs(logsResp, options);
}
};

exports.command = 'logs [endpoint]';
exports.describe = 'get logs for a function';

exports.handler = async options => {
loadAndValidateOptions(options);

const { latest, functionName } = options;

const accountId = getAccountId(options);

trackCommandUsage('logs', { latest }, accountId);

if (functionName) {
appFunctionLog(accountId, options);
} else {
endpointLog(accountId, options);
}
};

exports.builder = yargs => {
yargs.positional('endpoint', {
describe: 'Serverless function endpoint',
type: 'string',
});
yargs
.options({
appPath: {
describe: 'path to the app',
type: 'string',
hidden: true,
},
functionName: {
describe: 'app function name',
type: 'string',
hidden: true,
},
latest: {
alias: 'l',
describe: 'retrieve most recent log only',
Expand All @@ -191,7 +180,8 @@ exports.builder = yargs => {
type: 'number',
},
})
.conflicts('follow', 'limit');
.conflicts('follow', 'limit')
.conflicts('functionName', 'endpoint');

yargs.example([
[
Expand Down
Loading

0 comments on commit 93d2d15

Please sign in to comment.