You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

290 lines
6.7 KiB

#!/usr/bin/env node
const qs = require('querystring')
const minimist = require('minimist')
const chalk = require('chalk')
const dateformat = require('dateformat')
const io = require('socket.io-client')
const Now = require('../lib')
const login = require('../lib/login')
const cfg = require('../lib/cfg')
const { handleError, error } = require('../lib/error')
const logo = require('../lib/utils/output/logo')
const { compare, deserialize } = require('../lib/logs')
const { maybeURL, normalizeURL, parseInstanceURL } = require('../lib/utils/url')
const argv = minimist(process.argv.slice(2), {
string: ['config', 'query', 'since', 'token', 'until'],
boolean: ['help', 'all', 'debug', 'f'],
alias: {
help: 'h',
all: 'a',
config: 'c',
debug: 'd',
token: 't',
query: 'q'
}
})
let deploymentIdOrURL = argv._[0]
const help = () => {
console.log(`
${chalk.bold(`${logo} now logs`)} <deploymentId|url>
${chalk.dim('Options:')}
-h, --help output usage information
-a, --all include access logs
-c ${chalk.bold.underline('FILE')}, --config=${chalk.bold.underline(
'FILE'
)} config file
-d, --debug debug mode [off]
-f 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 if the f option is enbled.
${chalk.dim('Examples:')}
${chalk.gray('–')} Print logs for the deployment ${chalk.dim(
'`deploymentId`'
)}
${chalk.cyan('$ now logs deploymentId')}
`)
}
if (argv.help || !deploymentIdOrURL) {
help()
process.exit(0)
}
// Options
const debug = argv.debug
const apiUrl = argv.url || 'https://api.zeit.co'
if (argv.config) {
cfg.setConfigFile(argv.config)
}
const limit = typeof argv.n === 'number' ? argv.n : 1000
const query = argv.query || ''
const follow = argv.f
const types = argv.all ? [] : ['command', 'stdout', 'stderr', 'exit']
let since
try {
since = argv.since ? toSerial(argv.since) : null
} catch (err) {
error(`Invalid date string: ${argv.since}`)
process.exit(1)
}
let until
try {
until = argv.until ? toSerial(argv.until) : null
} catch (err) {
error(`Invalid date string: ${argv.until}`)
process.exit(1)
}
let instanceId
if (maybeURL(deploymentIdOrURL)) {
const normalizedURL = normalizeURL(deploymentIdOrURL)
if (normalizedURL.includes('/')) {
error(`Invalid deployment url: can't include path (${deploymentIdOrURL})`)
process.exit(1)
}
;[deploymentIdOrURL, instanceId] = parseInstanceURL(normalizedURL)
}
Promise.resolve()
.then(async () => {
const config = await cfg.read({ token: argv.token })
let token
try {
token = config.token || (await login(apiUrl))
} catch (err) {
error(`Authentication error – ${err.message}`)
process.exit(1)
}
await printLogs({ token, config })
})
.catch(err => {
handleError(err)
process.exit(1)
})
async function printLogs({ token, config: { currentTeam } }) {
let buf = []
let init = false
let lastLog
if (!follow) {
onLogs(await fetchLogs({ token, currentTeam, since, until }))
return
}
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
})
socket.on('error', err => {
if (debug) {
console.log('> [debug] Socket error', err.stack)
}
})
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)
process.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)
}