Logs CLI:

- Can print logs for one namespace or all namespaces
- Can sort logs since one start timestamp/until one end timestamp
- The sort results can be limited by one value
This commit is contained in:
wescoeur 2015-11-18 09:21:27 +01:00
parent b03f38ff22
commit 39090c2a22
5 changed files with 246 additions and 1 deletions

10
bin/xo-server-logs Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env node
'use strict'
// ===================================================================
// Better stack traces if possible.
require('../better-stacks')
require('exec-promise')(require('../dist/logs-cli'))

View File

@ -67,6 +67,7 @@
"julien-f-unzip": "^0.2.1", "julien-f-unzip": "^0.2.1",
"kindof": "^2.0.0", "kindof": "^2.0.0",
"level": "^1.3.0", "level": "^1.3.0",
"level-party": "^2.1.2",
"level-sublevel": "^6.5.2", "level-sublevel": "^6.5.2",
"lodash.assign": "^3.0.0", "lodash.assign": "^3.0.0",
"lodash.bind": "^3.0.0", "lodash.bind": "^3.0.0",
@ -76,6 +77,7 @@
"lodash.find": "^3.0.0", "lodash.find": "^3.0.0",
"lodash.findindex": "^3.0.0", "lodash.findindex": "^3.0.0",
"lodash.foreach": "^3.0.1", "lodash.foreach": "^3.0.1",
"lodash.get": "^3.7.0",
"lodash.has": "^3.0.0", "lodash.has": "^3.0.0",
"lodash.includes": "^3.1.1", "lodash.includes": "^3.1.1",
"lodash.isarray": "^3.0.0", "lodash.isarray": "^3.0.0",
@ -89,8 +91,11 @@
"lodash.sortby": "^3.1.4", "lodash.sortby": "^3.1.4",
"lodash.startswith": "^3.0.1", "lodash.startswith": "^3.0.1",
"make-error": "^1", "make-error": "^1",
"micromatch": "^2.3.2",
"minimist": "^1.2.0",
"ms": "^0.7.1", "ms": "^0.7.1",
"multikey-hash": "^1.0.1", "multikey-hash": "^1.0.1",
"ndjson": "^1.4.3",
"partial-stream": "0.0.0", "partial-stream": "0.0.0",
"passport": "^0.3.0", "passport": "^0.3.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",

54
src/glob-matcher.js Normal file
View File

@ -0,0 +1,54 @@
// See: https://gist.github.com/julien-f/5b9a3537eb82a34b04e2
var matcher = require('micromatch').matcher
module.exports = function globMatcher (patterns, opts) {
if (!Array.isArray(patterns)) {
if (patterns[0] === '!') {
var m = matcher(patterns.slice(1), opts)
return function (string) {
return !m(string)
}
} else {
return matcher(patterns, opts)
}
}
var noneMustMatch = []
var anyMustMatch = []
// TODO: could probably be optimized by combining all positive patterns (and all negative patterns) as a single matcher.
for (var i = 0, n = patterns.length; i < n; ++i) {
var pattern = patterns[i]
if (pattern[0] === '!') {
noneMustMatch.push(matcher(pattern.slice(1), opts))
} else {
anyMustMatch.push(matcher(pattern, opts))
}
}
var nNone = noneMustMatch.length
var nAny = anyMustMatch.length
return function (string) {
var i
for (i = 0; i < nNone; ++i) {
if (noneMustMatch[i](string)) {
return false
}
}
if (nAny === 0) {
return true
}
for (i = 0; i < nAny; ++i) {
if (anyMustMatch[i](string)) {
return true
}
}
return false
}
}

176
src/logs-cli.js Normal file
View File

@ -0,0 +1,176 @@
import appConf from 'app-conf'
import get from 'lodash.get'
import highland from 'highland'
import levelup from 'level-party'
import ndjson from 'ndjson'
import parseArgs from 'minimist'
import sublevel from 'level-sublevel'
import util from 'util'
import {forEach} from './utils'
import globMatcher from './glob-matcher'
// ===================================================================
async function printLogs (db, args) {
let stream = highland(db.createReadStream({reverse: true}))
if (args.since) {
stream = stream.filter(({value}) => (value.time >= args.since))
}
if (args.until) {
stream = stream.filter(({value}) => (value.time <= args.until))
}
const fields = Object.keys(args.matchers)
if (fields.length > 0) {
stream = stream.filter(({value}) => {
for (const field of fields) {
const fieldValue = get(value, field)
if (fieldValue === undefined || !args.matchers[field](fieldValue)) {
return false
}
}
return true
})
}
stream = stream.take(args.limit)
if (args.json) {
stream = highland(stream.pipe(ndjson.serialize()))
.each(value => {
process.stdout.write(value)
})
} else {
stream = stream.each(value => {
console.log(util.inspect(value, { depth: null }))
})
}
return new Promise(resolve => {
stream.done(resolve)
})
}
// ===================================================================
function helper () {
console.error(
`Usage:
xo-server-logs --help, -h
xo-server-logs [--json] [--limit=<limit>] [--since=<date>] [--until=<date>] [<pattern>...]
--help
Display this help message.
--json
Display the results as new line delimited JSON for consumption by another program.
--limit=<limit>, -n <limit>
Limit the number of results to be displayed (default 100)
--since=<date>, --until=<date>
Start showing entries on or newer than the specified date, or on or older than the specified date.
<date> should use the format \`YYYY-MM-DD\`.
<pattern>
Patterns can be used to filter the entries.
<pattern> have the following format \`<field>=<value>\`/\`<field>\`.`
)
}
// ===================================================================
function getArgs () {
const stringArgs = ['since', 'until', 'limit']
const args = parseArgs(process.argv.slice(2), {
string: stringArgs,
boolean: ['help', 'json'],
default: {
limit: 100,
json: false,
help: false
},
alias: {
limit: 'n',
help: 'h'
}
})
const patterns = {}
for (let value of args._) {
value = String(value)
const i = value.indexOf('=')
if (i !== -1) {
const field = value.slice(0, i)
const pattern = value.slice(i + 1)
patterns[pattern]
? patterns[field].push(pattern)
: patterns[field] = [ pattern ]
} else if (!patterns[value]) {
patterns[value] = null
}
}
const trueFunction = () => true
args.matchers = {}
for (const field in patterns) {
const values = patterns[field]
args.matchers[field] = (values === null) ? trueFunction : globMatcher(values)
}
// Warning: minimist makes one array of values if the same option is used many times.
// (But only for strings args, not boolean)
forEach(stringArgs, arg => {
if (args[arg] instanceof Array) {
throw new Error(`error: too many values for ${arg} argument`)
}
})
;['since', 'until'].forEach(arg => {
if (args[arg] !== undefined) {
args[arg] = Date.parse(args[arg])
if (isNaN(args[arg])) {
throw new Error(`error: bad ${arg} timestamp format`)
}
}
})
if (isNaN(args.limit = +args.limit)) {
throw new Error('error: limit is not a valid number')
}
return args
}
// ===================================================================
export default async function main () {
const args = getArgs()
if (args.help) {
helper()
return
}
const config = await appConf.load('xo-server', {
ignoreUnknownFormats: true
})
const db = sublevel(levelup(
`${config.datadir}/leveldb`,
{ valueEncoding: 'json' }
)).sublevel('logs')
return printLogs(db, args)
}

View File

@ -8,7 +8,7 @@ import fs from 'fs-promise'
import includes from 'lodash.includes' import includes from 'lodash.includes'
import isFunction from 'lodash.isfunction' import isFunction from 'lodash.isfunction'
import isString from 'lodash.isstring' import isString from 'lodash.isstring'
import levelup from 'level' import levelup from 'level-party'
import sortBy from 'lodash.sortby' import sortBy from 'lodash.sortby'
import startsWith from 'lodash.startswith' import startsWith from 'lodash.startswith'
import sublevel from 'level-sublevel' import sublevel from 'level-sublevel'