Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New manifest and action level logs with new strip and tail options #303

Merged
merged 7 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 52 additions & 8 deletions src/commands/app/logs.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ governing permissions and limitations under the License.
const { flags } = require('@oclif/command')
// const { cli } = require('cli-ux')
const BaseCommand = require('../../BaseCommand')
const { wrapError, getLogs } = require('../../lib/app-helper')
const { wrapError } = require('../../lib/app-helper')
const rtLib = require('@adobe/aio-lib-runtime')
const fs = require('fs-extra')

class Logs extends BaseCommand {
async run () {
const { flags } = this.parse(Logs)
const config = this.getAppConfig()
if (!fs.existsSync('manifest.yml')) {
this.error(wrapError(new Error('no manifest.yml')))
}

if (flags.limit < 1) {
this.log('--limit should be > 0, using --limit=1')
Expand All @@ -27,10 +33,25 @@ class Logs extends BaseCommand {
flags.limit = 50
}

try {
const config = this.getAppConfig()
let filterActions = []
if (flags.action) {
let actionName = flags.action
if (!actionName.includes('/')) {
actionName = config.ow.package + '/' + actionName
}
filterActions = [actionName]
} else {
Object.entries(config.manifest.full.packages).forEach((packageTuple) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a little gnarly, not sure it could be any better though

packageTuple[0] = packageTuple[0].replace(/__APP_PACKAGE__/g, config.ow.package)

Object.keys(packageTuple[1].actions).forEach((actionName) => {
filterActions.push(packageTuple[0] + '/' + actionName)
})
})
}

await getLogs(config, flags.limit, this.log)
try {
await rtLib.printActionLogs(config, this.log, flags.limit, filterActions, flags.strip, flags.poll || flags.tail || flags.watch)
} catch (error) {
this.error(wrapError(error))
}
Expand All @@ -46,11 +67,34 @@ Logs.flags = {
description: 'Limit number of activations to fetch logs from ( 1-50 )',
default: 1,
char: 'l'
}),
action: flags.string({
description: 'Fetch logs for a specific action',
char: 'a'
}),
strip: flags.boolean({
char: 'r',
description: 'strip timestamp information and output first line only',
default: false
}),
tail: flags.boolean({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is tail our final choice? should we alias it with watch?
what is the point of adding a default for -a? if they want to get logs for an action or actions, they would need to specify it ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thx @purplecabbage . Removed action default and added aliases.

description: 'Fetch logs continuously',
char: 't',
default: false,
exclusive: ['watch', 'poll']
}),
watch: flags.boolean({
description: 'Fetch logs continuously',
default: false,
char: 'w',
exclusive: ['tail', 'poll']
}),
poll: flags.boolean({
description: 'Fetch logs continuously',
default: false,
char: 'o',
exclusive: ['watch', 'tail']
})
}

// Logs.args = [
// ...BaseCommand.args
// ]

module.exports = Logs
38 changes: 0 additions & 38 deletions src/lib/app-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const dotenv = require('dotenv')
const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:lib-app-helper', { provider: 'debug' })
const { getToken, context } = require('@adobe/aio-lib-ims')
const { CLI } = require('@adobe/aio-lib-ims/src/context')
const RuntimeLib = require('@adobe/aio-lib-runtime')
const fetch = require('node-fetch')

/** @private */
Expand Down Expand Up @@ -86,42 +85,6 @@ async function getCliInfo () {
return { accessToken, env }
}

async function getLogs (config, limit, logger, startTime = 0) {
// check for runtime credentials
RuntimeLib.utils.checkOpenWhiskCredentials(config)
const runtime = await RuntimeLib.init({
// todo make this.config.ow compatible with Openwhisk config
apihost: config.ow.apihost,
apiversion: config.ow.apiversion,
api_key: config.ow.auth,
namespace: config.ow.namespace
})

// get activations
const listOptions = { limit: limit, skip: 0 }
const logFunc = logger || console.log
const activations = await runtime.activations.list(listOptions)
let lastActivationTime = 0
// console.log('activations = ', activations)
for (let i = (activations.length - 1); i >= 0; i--) {
const activation = activations[i]
lastActivationTime = activation.start
if (lastActivationTime > startTime) {
const results = await runtime.activations.logs({ activationId: activation.activationId })
// console.log('results = ', results)
// send fetched logs to console
if (results.logs.length > 0) {
logFunc(activation.name + ':' + activation.activationId)
results.logs.forEach(function (log) {
logFunc(log)
})
logFunc()
}
}
}
return { lastActivationTime }
}

function getActionUrls (config, isRemoteDev = false, isLocalDev = false) {
// set action urls
// action urls {name: url}, if !LocalDev subdomain uses namespace
Expand Down Expand Up @@ -360,7 +323,6 @@ module.exports = {
wrapError,
getCliInfo,
getActionUrls,
getLogs,
removeProtocolFromURL,
urlJoin,
checkFile,
Expand Down
6 changes: 4 additions & 2 deletions src/lib/runDev.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ governing permissions and limitations under the License.
*/
/* eslint-disable no-template-curly-in-string */
const aioLogger = require('@adobe/aio-lib-core-logging')('@adobe/aio-cli-plugin-app:runDev', { provider: 'debug' })
const rtLibUtils = require('@adobe/aio-lib-runtime').utils
const rtLib = require('@adobe/aio-lib-runtime')
const rtLibUtils = rtLib.utils
const path = require('path')
const fs = require('fs-extra')

Expand Down Expand Up @@ -232,7 +233,8 @@ async function runDev (args = [], config, options = {}, log) {
async function logListener (args) {
if (!args.resources.stopFetchLogs) {
try {
const ret = await utils.getLogs(args.config, logOptions.limit || 1, console.log, logOptions.startTime)
// TODO : Is is better to just tail ?
const ret = await rtLib.printActionLogs(args.config, console.log, logOptions.limit || 1, [], false, false, undefined, logOptions.startTime)
logOptions.limit = 30
logOptions.startTime = ret.lastActivationTime
} catch (e) {
Expand Down
3 changes: 2 additions & 1 deletion test/__mocks__/@adobe/aio-lib-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ const init = jest.fn().mockReturnValue(mockRtLibInstance)
const mockActionMethods = {
buildActions: jest.fn(),
deployActions: jest.fn(),
undeployActions: jest.fn()
undeployActions: jest.fn(),
printActionLogs: jest.fn()
}
module.exports = {
utils: {
Expand Down
1 change: 1 addition & 0 deletions test/commands/app/init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const path = require('path')

const TheCommand = require('../../../src/commands/app/init')
const BaseCommand = require('../../../src/BaseCommand')
const runtimeLib = require('@adobe/aio-lib-runtime') // eslint-disable-line no-unused-vars
const importLib = require('../../../src/lib/import')
jest.mock('../../../src/lib/import')

Expand Down
85 changes: 74 additions & 11 deletions test/commands/app/logs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,30 @@ governing permissions and limitations under the License.
const TheCommand = require('../../../src/commands/app/logs')
const BaseCommand = require('../../../src/BaseCommand')

const mockFS = require('fs-extra')

jest.mock('../../../src/lib/app-helper.js')
const helpers = require('../../../src/lib/app-helper.js')

const mockRuntimeLib = require('@adobe/aio-lib-runtime')
const printActionLogs = mockRuntimeLib.printActionLogs

const fakeAppConfig = {
manifest: {
full: {
packages: {
jesttestpackage: {
actions: {
hello: {}
}
}
}
}
},
ow: {
package: 'jesttestpackage'
}
}
describe('interface', () => {
test('exports', async () => {
expect(typeof TheCommand).toEqual('function')
Expand All @@ -40,66 +61,108 @@ describe('interface', () => {

describe('run', () => {
beforeEach(() => {
helpers.getLogs.mockReset()
printActionLogs.mockReset()
helpers.wrapError.mockReset()
mockFS.existsSync.mockReset()
})

test('no flags, sets limit to 1', async () => {
mockFS.existsSync.mockReturnValue(true)
const command = new TheCommand([])
command.appConfig = { fake: 'config' }
command.appConfig = fakeAppConfig
command.error = jest.fn()
command.log = jest.fn()

await command.run()
expect(helpers.getLogs).toHaveBeenCalledWith({ fake: 'config' }, 1, command.log)
expect(printActionLogs).toHaveBeenCalledWith(command.appConfig, command.log, 1, ['jesttestpackage/hello'], false, false)
expect(command.error).not.toHaveBeenCalled()
})

test('--limit < 1, sets limit to 1', async () => {
mockFS.existsSync.mockReturnValue(true)
const command = new TheCommand(['--limit', '-1'])
command.appConfig = { fake: 'config' }
command.appConfig = fakeAppConfig
command.error = jest.fn()
command.log = jest.fn()

await command.run()
expect(command.log).toHaveBeenCalledWith(expect.stringContaining('using --limit=1'))
expect(helpers.getLogs).toHaveBeenCalledWith({ fake: 'config' }, 1, command.log)
expect(printActionLogs).toHaveBeenCalledWith(fakeAppConfig, command.log, 1, ['jesttestpackage/hello'], false, false)
expect(command.error).not.toHaveBeenCalled()
})

test('--limit > 50, sets limit to 50', async () => {
mockFS.existsSync.mockReturnValue(true)
const command = new TheCommand(['--limit', '51'])
command.appConfig = { fake: 'config' }
command.appConfig = fakeAppConfig
command.error = jest.fn()
command.log = jest.fn()

await command.run()
expect(command.log).toHaveBeenCalledWith(expect.stringContaining('using --limit=50'))
expect(helpers.getLogs).toHaveBeenCalledWith({ fake: 'config' }, 50, command.log)
expect(printActionLogs).toHaveBeenCalledWith(fakeAppConfig, command.log, 50, ['jesttestpackage/hello'], false, false)
expect(command.error).not.toHaveBeenCalled()
})

test('--limit 32', async () => {
mockFS.existsSync.mockReturnValue(true)
const command = new TheCommand(['--limit', '32'])
command.appConfig = { fake: 'config' }
command.appConfig = fakeAppConfig
command.error = jest.fn()
command.log = jest.fn()

await command.run()
expect(printActionLogs).toHaveBeenCalledWith(fakeAppConfig, command.log, 32, ['jesttestpackage/hello'], false, false)
expect(command.error).not.toHaveBeenCalled()
})

test('--action without including package name', async () => {
mockFS.existsSync.mockReturnValue(true)
const command = new TheCommand(['--action', 'hello'])
command.appConfig = fakeAppConfig
command.error = jest.fn()
command.log = jest.fn()

await command.run()
expect(helpers.getLogs).toHaveBeenCalledWith({ fake: 'config' }, 32, command.log)
expect(printActionLogs).toHaveBeenCalledWith(fakeAppConfig, command.log, 1, ['jesttestpackage/hello'], false, false)
expect(command.error).not.toHaveBeenCalled()
})

test('--action including package name', async () => {
mockFS.existsSync.mockReturnValue(true)
const command = new TheCommand(['--action', 'pkg1/hello'])
command.appConfig = fakeAppConfig
command.error = jest.fn()
command.log = jest.fn()

await command.run()
expect(printActionLogs).toHaveBeenCalledWith(fakeAppConfig, command.log, 1, ['pkg1/hello'], false, false)
expect(command.error).not.toHaveBeenCalled()
})

test('error while getting logs', async () => {
mockFS.existsSync.mockReturnValue(true)
const command = new TheCommand([])
command.appConfig = { fake: 'config' }
command.appConfig = fakeAppConfig
command.error = jest.fn()
command.log = jest.fn()
const theerror = new Error('I do not like logs')
helpers.getLogs.mockRejectedValue(theerror)
printActionLogs.mockRejectedValue(theerror)
helpers.wrapError.mockReturnValue('wrapped error')
await command.run()
expect(command.error).toHaveBeenCalledWith('wrapped error')
expect(helpers.wrapError).toHaveBeenCalledWith(theerror)
})

test('error no manifest', async () => {
mockFS.existsSync.mockReturnValue(false)
const command = new TheCommand([])
command.appConfig = fakeAppConfig
command.error = jest.fn()
command.log = jest.fn()
helpers.wrapError.mockReturnValue('wrapped error')
await command.run()
expect(command.error).toHaveBeenCalledWith('wrapped error')
expect(helpers.wrapError).toHaveBeenCalledWith(new Error('no manifest.yml'))
})
})
Loading