#!/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`)} ${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) }