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:
parent
b03f38ff22
commit
39090c2a22
10
bin/xo-server-logs
Executable file
10
bin/xo-server-logs
Executable 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'))
|
@ -67,6 +67,7 @@
|
||||
"julien-f-unzip": "^0.2.1",
|
||||
"kindof": "^2.0.0",
|
||||
"level": "^1.3.0",
|
||||
"level-party": "^2.1.2",
|
||||
"level-sublevel": "^6.5.2",
|
||||
"lodash.assign": "^3.0.0",
|
||||
"lodash.bind": "^3.0.0",
|
||||
@ -76,6 +77,7 @@
|
||||
"lodash.find": "^3.0.0",
|
||||
"lodash.findindex": "^3.0.0",
|
||||
"lodash.foreach": "^3.0.1",
|
||||
"lodash.get": "^3.7.0",
|
||||
"lodash.has": "^3.0.0",
|
||||
"lodash.includes": "^3.1.1",
|
||||
"lodash.isarray": "^3.0.0",
|
||||
@ -89,8 +91,11 @@
|
||||
"lodash.sortby": "^3.1.4",
|
||||
"lodash.startswith": "^3.0.1",
|
||||
"make-error": "^1",
|
||||
"micromatch": "^2.3.2",
|
||||
"minimist": "^1.2.0",
|
||||
"ms": "^0.7.1",
|
||||
"multikey-hash": "^1.0.1",
|
||||
"ndjson": "^1.4.3",
|
||||
"partial-stream": "0.0.0",
|
||||
"passport": "^0.3.0",
|
||||
"passport-local": "^1.0.0",
|
||||
|
54
src/glob-matcher.js
Normal file
54
src/glob-matcher.js
Normal 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
176
src/logs-cli.js
Normal 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)
|
||||
}
|
@ -8,7 +8,7 @@ import fs from 'fs-promise'
|
||||
import includes from 'lodash.includes'
|
||||
import isFunction from 'lodash.isfunction'
|
||||
import isString from 'lodash.isstring'
|
||||
import levelup from 'level'
|
||||
import levelup from 'level-party'
|
||||
import sortBy from 'lodash.sortby'
|
||||
import startsWith from 'lodash.startswith'
|
||||
import sublevel from 'level-sublevel'
|
||||
|
Loading…
Reference in New Issue
Block a user