now-flow
Version:
Add deployment workflows to Zeit now
311 lines (263 loc) • 7.11 kB
JavaScript
// Native
const qs = require('querystring')
// Packages
const mri = require('mri')
const chalk = require('chalk')
const dateformat = require('dateformat')
const io = require('socket.io-client')
/*eslint-disable */
const getProcess = () => process
/*eslint-enable */
// Utilities
const Now = require('../util')
const { handleError, error } = require('../util/error')
const logo = require('../../../util/output/logo')
const { compare, deserialize } = require('../util/logs')
const { maybeURL, normalizeURL, parseInstanceURL } = require('../../../util/url')
const exit = require('../../../util/exit')
const help = () => {
console.log(`
${chalk.bold(`${logo} now logs`)} <deploymentId|url>
${chalk.dim('Options:')}
-h, --help Output usage information
-a, --all Include access logs
-A ${chalk.bold.underline('FILE')}, --local-config=${chalk.bold.underline(
'FILE'
)} Path to the local ${'`now.json`'} file
-Q ${chalk.bold.underline('DIR')}, --global-config=${chalk.bold.underline(
'DIR'
)} Path to the global ${'`.now`'} directory
-d, --debug Debug mode [off]
-f, --follow Wait for additional data [off]
-n ${chalk.bold.underline('NUMBER')} Number of logs [1000]
-q ${chalk.bold.underline('QUERY')}, --query=${chalk.bold.underline(
'QUERY'
)} Search query
-t ${chalk.bold.underline('TOKEN')}, --token=${chalk.bold.underline(
'TOKEN'
)} Login token
--since=${chalk.bold.underline(
'SINCE'
)} Only return logs after date (ISO 8601)
--until=${chalk.bold.underline(
'UNTIL'
)} Only return logs before date (ISO 8601), ignored for ${'`-f`'}
-T, --team Set a custom team scope
${chalk.dim('Examples:')}
${chalk.gray('–')} Print the logs for the deployment ${chalk.dim(
'`deploymentId`'
)}
${chalk.cyan('$ now logs deploymentId')}
`)
}
let argv
let deploymentIdOrURL
let debug
let apiUrl
let limit
let query
let follow
let types
let since
let until
let instanceId
const main = async ctx => {
argv = mri(ctx.argv.slice(2), {
string: ['query', 'since', 'until'],
boolean: ['help', 'all', 'debug', 'follow'],
alias: {
help: 'h',
all: 'a',
debug: 'd',
query: 'q',
follow: 'f'
}
})
argv._ = argv._.slice(1)
deploymentIdOrURL = argv._[0]
if (argv.help || !deploymentIdOrURL || deploymentIdOrURL === 'help') {
help()
await exit(0)
}
try {
since = argv.since ? toSerial(argv.since) : null
} catch (err) {
error(`Invalid date string: ${argv.since}`)
getProcess().exit(1)
}
try {
until = argv.until ? toSerial(argv.until) : null
} catch (err) {
error(`Invalid date string: ${argv.until}`)
getProcess().exit(1)
}
if (maybeURL(deploymentIdOrURL)) {
const normalizedURL = normalizeURL(deploymentIdOrURL)
if (normalizedURL.includes('/')) {
error(`Invalid deployment url: can't include path (${deploymentIdOrURL})`)
getProcess().exit(1)
}
[deploymentIdOrURL, instanceId] = parseInstanceURL(normalizedURL)
}
debug = argv.debug
apiUrl = ctx.apiUrl
limit = typeof argv.n === 'number' ? argv.n : 1000
query = argv.query || ''
follow = argv.f
types = argv.all ? [] : ['command', 'stdout', 'stderr', 'exit']
const {authConfig: { credentials }, config: { sh }} = ctx
const {token} = credentials.find(item => item.provider === 'sh')
return printLogs({ token, sh })
}
module.exports = async ctx => {
try {
await main(ctx)
} catch (err) {
handleError(err)
getProcess().exit(1)
}
}
function printLogs({ token, sh: { currentTeam } }) {
return new Promise(async (resolve, reject) => {
let buf = []
let init = false
let lastLog
if (!follow) {
onLogs(await fetchLogs({ token, currentTeam, since, until }))
resolve()
}
const isURL = deploymentIdOrURL.includes('.')
const q = qs.stringify({
deploymentId: isURL ? '' : deploymentIdOrURL,
host: isURL ? deploymentIdOrURL : '',
instanceId,
types: types.join(','),
query
})
const socket = io(`https://log-io.zeit.co?${q}`)
socket.on('connect', () => {
if (debug) {
console.log('> [debug] Socket connected')
}
})
socket.on('auth', callback => {
if (debug) {
console.log('> [debug] Socket authenticate')
}
callback(token)
})
socket.on('ready', () => {
if (debug) {
console.log('> [debug] Socket ready')
}
// For the case socket reconnected
const _since = lastLog ? lastLog.serial : since
fetchLogs({ token, currentTeam, since: _since }).then(logs => {
init = true
const m = {}
logs.concat(buf.map(b => b.log)).forEach(l => {
m[l.id] = l
})
buf = []
onLogs(Object.values(m))
})
})
socket.on('logs', l => {
const log = deserialize(l)
let timer
if (init) {
// Wait for other logs for a while
// and sort them in the correct order
timer = setTimeout(() => {
buf.sort((a, b) => compare(a.log, b.log))
const idx = buf.findIndex(b => b.log.id === log.id)
buf.slice(0, idx + 1).forEach(b => {
clearTimeout(b.timer)
onLog(b.log)
})
buf = buf.slice(idx + 1)
}, 300)
}
buf.push({ log, timer })
})
socket.on('disconnect', () => {
if (debug) {
console.log('> [debug] Socket disconnect')
}
init = false
reject(new Error('Socket disconnected'))
})
socket.on('error', err => {
if (debug) {
console.log('> [debug] Socket error', err.stack)
}
reject(err)
})
function onLogs(logs) {
logs.sort(compare).forEach(onLog)
}
function onLog(log) {
lastLog = log
printLog(log)
}
})
}
function printLog(log) {
let data
const obj = log.object
if (log.type === 'request') {
data =
`REQ "${obj.method} ${obj.uri} ${obj.protocol}"` +
` ${obj.remoteAddr} - ${obj.remoteUser || ''}` +
` "${obj.referer || ''}" "${obj.userAgent || ''}"`
} else if (log.type === 'response') {
data =
`RES "${obj.method} ${obj.uri} ${obj.protocol}"` +
` ${obj.status} ${obj.bodyBytesSent}`
} else {
data = obj
? JSON.stringify(obj, null, 2)
: (log.text || '').replace(/\n$/, '')
}
const date = dateformat(log.date, 'mm/dd hh:MM TT')
data.split('\n').forEach((line, i) => {
if (i === 0) {
console.log(`${chalk.dim(date)} ${line}`)
} else {
console.log(`${repeat(' ', date.length)} ${line}`)
}
})
}
async function fetchLogs({ token, currentTeam, since, until } = {}) {
const now = new Now({ apiUrl, token, debug, currentTeam })
let logs
try {
logs = await now.logs(deploymentIdOrURL, {
instanceId,
types,
limit,
query,
since,
until
})
} catch (err) {
handleError(err)
getProcess().exit(1)
} finally {
now.close()
}
return logs.map(deserialize)
}
function repeat(s, n) {
return new Array(n + 1).join(s)
}
function toSerial(datestr) {
const t = Date.parse(datestr)
if (isNaN(t)) {
throw new TypeError('Invalid date string')
}
const pidLen = 19
const seqLen = 19
return t + repeat('0', pidLen + seqLen)
}