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.
271 lines
6.5 KiB
271 lines
6.5 KiB
8 years ago
|
#!/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 } = 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);
|
||
|
}
|
||
|
|
||
|
if (maybeURL(deploymentIdOrURL)) {
|
||
|
deploymentIdOrURL = normalizeURL(deploymentIdOrURL);
|
||
|
}
|
||
|
|
||
|
Promise.resolve()
|
||
|
.then(async () => {
|
||
|
const config = await cfg.read();
|
||
|
|
||
|
let token;
|
||
|
try {
|
||
|
token = argv.token || config.token || login(apiUrl);
|
||
|
} catch (err) {
|
||
|
error(`Authentication error – ${err.message}`);
|
||
|
process.exit(1);
|
||
|
}
|
||
|
|
||
|
await printLogs({token, config});
|
||
|
})
|
||
|
.catch(err => {
|
||
|
error(`Unknown error: ${err.stack}`);
|
||
|
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 : '',
|
||
|
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, {
|
||
|
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);
|
||
|
}
|