Comparing version 0.1.2 to 0.2.0
@@ -8,2 +8,16 @@ # GoLog Changelog | ||
## [v0.2.0] - 2020-08-23 | ||
### Added | ||
- log coloring | ||
- auto-parsing of specific fields | ||
- function to filter out by log level | ||
- function to filter out by log type | ||
### Changed | ||
- entire interface for logging to stdout | ||
### Removed | ||
- support for direct file/stream writing | ||
## [v0.1.2] - 2019-10-15 | ||
@@ -26,1 +40,2 @@ | ||
[v0.1.2]: https://gitlab.com/GCSBOSS/golog/-/tags/v0.1.2 | ||
[v0.2.0]: https://gitlab.com/GCSBOSS/golog/-/tags/v0.2.0 |
173
lib/main.js
@@ -1,111 +0,124 @@ | ||
const { EventEmitter } = require('events'); | ||
const assert = require('assert'); | ||
const util = require('util'); | ||
const fs = require('fs'); | ||
const LEVELS = { | ||
debug: { w: 0, c: '\x1b[30;5;1m' }, | ||
info: { w: 1, c: '\x1b[0m' }, | ||
warn: { w: 2, c: '\x1b[33m' }, | ||
error: { w: 3, c: '\x1b[31m' }, | ||
fatal: { w: 4, c: '\x1b[41;5;1m' } | ||
}; | ||
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }; | ||
function output(entry){ | ||
let env = process.env.NODE_ENV; | ||
function formatErrData(err){ | ||
// Output friendly log for dev or undfined env | ||
/* istanbul ignore next */ | ||
if(!env || env === 'development'){ | ||
let uword = (entry.type == 'event' ? entry.level : entry.type).toUpperCase(); | ||
entry.time.setMinutes(entry.time.getMinutes() - entry.time.getTimezoneOffset()); | ||
let utime = entry.time.toISOString().slice(11, -5); | ||
console.log(LEVELS[entry.level].c + utime + ': ' + uword + ' - ' + entry.msg + '\x1b[0m'); | ||
} | ||
// Output complete JSON log for production and staging | ||
/* istanbul ignore next */ | ||
else if(env !== 'testing') | ||
console.log(JSON.stringify(entry)); | ||
} | ||
function parseErr({ err }){ | ||
if(!err) | ||
return {}; | ||
if(!(err instanceof Error)) | ||
err = new Error(err); | ||
let stack = err.stack.split(/[\r\n]+\s*/g); | ||
return { | ||
err: null, | ||
code: err.code, | ||
name: err.constructor.name, | ||
class: err.constructor.name, | ||
message: err.message, | ||
stack: err.stack.split(/[\r\n]+\s*/g).slice(1, -1) | ||
}; | ||
stack: stack.slice(1, -1), | ||
msg: stack[0] + ' ' + stack[1] | ||
} | ||
} | ||
function formatReqData(req){ | ||
function parseReq({ req }){ | ||
if(!req) | ||
return {}; | ||
return { | ||
req: null, | ||
method: req.method, | ||
path: req.url, | ||
host: req.headers.host, | ||
agent: req.headers['user-agent'] | ||
}; | ||
path: req.url || req.path, | ||
host: req.getHeader('host'), | ||
agent: req.getHeader('user-agent'), | ||
type: 'request', | ||
msg: 'Received ' + req.method + ' request to ' + req.url | ||
} | ||
} | ||
function formatData(data){ | ||
if(data.err instanceof Error) | ||
data.err = formatErrData(data.err); | ||
if(typeof data.req === 'object') | ||
data.req = formatReqData(data.req); | ||
return data; | ||
function parseRes({ res }){ | ||
if(!res) | ||
return {}; | ||
let req = res.req; | ||
return { | ||
res: null, | ||
level: res.statusCode > 499 ? 'warn' : 'debug', | ||
path: req.url || req.path, | ||
status: res.statusCode, | ||
method: req.method, | ||
type: 'response', | ||
msg: 'Sent ' + res.statusCode + ' response to ' + req.method + ' ' + req.url | ||
}; | ||
} | ||
function parseEntry(logger, level, args) { | ||
function getEntry(level, args){ | ||
let data = typeof args[0] == 'object' ? args.shift() : {}; | ||
let msg = util.format(...args); | ||
let type = data.type || 'event'; | ||
let time = (new Date()).toISOString(); | ||
/* istanbul ignore next */ | ||
let pid = process.pid != 1 ? process.pid : null; | ||
let data = {}; | ||
if(typeof args[0] == 'object') | ||
data = formatData(args.shift()); | ||
Object.assign(data, ...this.parsers.map(p => p(data))); | ||
let msg = args.shift(); | ||
msg = util.format(msg, ...args); | ||
if(logger.mode == 'minimal') | ||
return `${time} - ${level} - ${msg}\r\n`; | ||
let obj = { ...data, name: logger.name, pid: process.pid, level, msg, time }; | ||
let spaces, breaks = '\r\n'; | ||
if(logger.mode == 'pretty'){ | ||
spaces = 2; | ||
breaks = '\r\n\r\n'; | ||
} | ||
return JSON.stringify(obj, undefined, spaces) + breaks; | ||
msg = msg || data.msg; | ||
return { level, type, ...data, msg, pid, time: new Date() }; | ||
} | ||
function log(level, ...args) { | ||
function log(level, ...args){ | ||
let entry = getEntry.apply(this, [ level, args ]); | ||
entry.app = this.conf.app; | ||
let entry = parseEntry(this, level, args); | ||
let badLevel = LEVELS[this.conf.level].w > LEVELS[level].w; | ||
let badType = this.conf.except.has(entry.type) || | ||
this.conf.only.size > 0 && !this.conf.only.has(entry.type); | ||
if(badType || badLevel) | ||
return false; | ||
Object.values(this.streams).forEach(s => { | ||
let l = LOG_LEVELS[s.level || this.level]; | ||
if(LOG_LEVELS[level] >= l) | ||
s.stream.write(entry, 'utf8'); | ||
}); | ||
output(entry); | ||
this.emit(level, entry); | ||
this.emit('entry', entry); | ||
return entry; | ||
} | ||
const DEFAULT_OPTS = { name: 'golog', mode: 'full', level: 'warn' }; | ||
module.exports = class GoLog { | ||
module.exports = class GoLog extends EventEmitter { | ||
constructor(conf = {}){ | ||
this.conf = conf || {}; | ||
this.conf.level = conf.level || 'debug'; | ||
this.conf.only = new Set([].concat(conf.only).filter(a => a)); | ||
this.conf.except = new Set([].concat(conf.except).filter(a => a)); | ||
this.parsers = [ parseErr, parseReq, parseRes ]; | ||
constructor(opts) { | ||
opts = { ...DEFAULT_OPTS, ...opts }; | ||
super(); | ||
this.on('error', Function.prototype); | ||
this.active = true; | ||
this.streams = {}; | ||
let op = conf === false ? () => false : log; | ||
this.name = opts.name; | ||
this.mode = opts.mode; | ||
this.level = opts.level; | ||
Object.keys(LOG_LEVELS).forEach( l => | ||
this[l] = log.bind(this, l)); | ||
if(opts.stream) | ||
this.addStream('main', opts.stream); | ||
else if(opts.file) | ||
this.addFile('main', opts.file); | ||
for(let l in LEVELS) | ||
this[l] = op.bind(this, l); | ||
} | ||
addStream(name, stream, level) { | ||
assert(stream.writable); | ||
this.streams[name] = { stream, level }; | ||
addParser(parser){ | ||
this.parsers.push(parser); | ||
} | ||
removeStream(name) { | ||
delete this.streams[name]; | ||
} | ||
addFile(name, file, level) { | ||
let stream = fs.createWriteStream(file, { flags: 'a' }); | ||
this.addStream(name, stream, level); | ||
} | ||
} | ||
}; |
{ | ||
"name": "golog", | ||
"version": "0.1.2", | ||
"description": "A light-weight heavily inspired logging library", | ||
"version": "0.2.0", | ||
"description": "A light-weight stdout logging library for NodeJS.", | ||
"main": "lib/main.js", | ||
@@ -16,6 +16,2 @@ "author": "Guilherme C. Souza", | ||
}, | ||
"devDependencies": { | ||
"memorystream": "^0.3.1", | ||
"tempper": "^0.1.1" | ||
}, | ||
"repository": { | ||
@@ -26,13 +22,13 @@ "type": "git", | ||
"keywords": [ | ||
"logging", | ||
"json", | ||
"pretty", | ||
"simlpe", | ||
"log", | ||
"bunyan", | ||
"logging", | ||
"debug", | ||
"file", | ||
"stream", | ||
"env", | ||
"parse", | ||
"req", | ||
"res", | ||
"stdout", | ||
"12factor", | ||
"format" | ||
] | ||
} |
# [GoLog](https://gitlab.com/GCSBOSS/golog) | ||
A light-weight heavily inspired logging library for NodeJS. Checkout the main features: | ||
A light-weight stdout logging library for NodeJS. Checkout the main features: | ||
- JSON output as well as simple _date-level-msg_ format. | ||
- Auto-format error and request objects. | ||
- JSON output by default | ||
- Automatic readable output format for dev environments | ||
- Auto-format error, request and repsonse objects. | ||
- Support for adding custom function to parse interesting input data | ||
- `printf`-like formatting on messages. | ||
- Automatic pid field on log entries. | ||
- Broadcast to many files/streams. | ||
- Individual logging level for each file/stream. | ||
- Log entries filtering by level or type (whitelist, blacklist) | ||
@@ -22,8 +23,2 @@ ## Get Started | ||
var log = new GoLog(); | ||
// Add a Writable Stream to receive log entries. | ||
log.addStream( 'my-stdout', process.stdout ); | ||
// Add a file where to write log entries. | ||
log.addFile( 'my-file', '/path/to/file' ); | ||
``` | ||
@@ -43,62 +38,26 @@ | ||
If you want to setup a logger with a single stream or file go with the one-liner. | ||
If you have many loggers going to the same place rely on setting their source. | ||
```js | ||
var slog = new GoLog({ stream: process.stdout }); | ||
var flog = new GoLog({ file: '/path/to/file' }); | ||
``` | ||
If you have many loggers writing to the same stream, name each logger for better visibility. | ||
```js | ||
var l1 = new GoLog({ name: 'log-a', stream: process.stdout }); | ||
var l1 = new GoLog({ app: 'log-a' }); | ||
l1.warn('Hey!'); | ||
var l2 = new GoLog({ name: 'log-b', stream: process.stdout }); | ||
var l2 = new GoLog({ app: 'log-b' }); | ||
l2.warn('Ho!'); | ||
``` | ||
Default loggers only log entries of level `warn` or higher. Change the level for an entire logger. | ||
Default loggers log entries of any level. Change the level for warn or higher. | ||
```js | ||
var log = new GoLog({ level: 'debug', stream: process.stdout }); | ||
var log = new GoLog({ level: 'warn' }); | ||
log.debug('Let\'s go!'); | ||
``` | ||
If you need these files right now, just change to a mode with better aesthetics. | ||
You can check/use log entries right off the bat. | ||
```js | ||
var log = new GoLog({ stream: process.stdout }); | ||
log.warn('This is an ugly JSON'); | ||
log.mode = 'pretty' | ||
log.warn('This is a cute JSON'); | ||
log.mode = 'minimal' | ||
log.warn('This is just readable'); | ||
``` | ||
If you need to stop logging to a given stream/file just use `removeStream`. | ||
```js | ||
var log = new GoLog({ stream: process.stdout }); | ||
log.addFile('some-file', '/path/to/file'); | ||
log.warn('This goes to stdout and file'); | ||
log.removeStream('main'); | ||
log.warn('This goes to file only'); | ||
log.removeStream('some-file'); | ||
log.warn('This goes nowhere'); | ||
``` | ||
If you happen to need to setup some listeners. | ||
```js | ||
var log = new GoLog(); | ||
log.on('fatal', ent => console.log('Show me once')); | ||
log.on('entry', ent => console.log('Show me twice')); | ||
log.fatal('The fatal entry'); | ||
log.debug('The debug entry'); | ||
let e1 = log.fatal('foo'); | ||
let e2 = log.debug('bar'); | ||
console.log(e1, e2)/ | ||
``` | ||
@@ -118,7 +77,7 @@ | ||
var log = new GoLog({ stream: process.stdout }); | ||
var log = new GoLog(); | ||
var server = http.createServer(function(req, res){ | ||
res.end(); | ||
log.warn({ req: req }, 'The message lives on'); | ||
log.warn({ req }, 'The message lives on'); | ||
}).listen(8765); | ||
@@ -129,2 +88,9 @@ | ||
You can create a disabled logger if you send `false` as the input parameter | ||
```js | ||
var log = new GoLog(false); | ||
log.info('Wont log this'); | ||
``` | ||
## Reporting Bugs | ||
@@ -131,0 +97,0 @@ If you have found any problems with this module, please: |
244
test/spec.js
@@ -1,134 +0,86 @@ | ||
const MemoryStream = require('memorystream'); | ||
const Tempper = require('tempper'); | ||
const assert = require('assert'); | ||
const fs = require('fs'); | ||
const Logger = require('../lib/main'); | ||
var stream, log; | ||
process.env.NODE_ENV = 'testing'; | ||
beforeEach(function(){ | ||
stream = new MemoryStream(); | ||
}); | ||
describe('Logging', function(){ | ||
var log; | ||
describe('Setup', function(){ | ||
it('Should store the given stream to write log entries [#addStream]', function(){ | ||
before(function(){ | ||
log = new Logger(); | ||
log.addStream('foobar', stream); | ||
assert.strictEqual(typeof log.streams.foobar, 'object'); | ||
}); | ||
it('Should create fs write stream from file path [#addFile]', function(done){ | ||
log = new Logger(); | ||
let tmp = new Tempper(); | ||
log.addFile('foobar', './foo.txt'); | ||
log.streams.foobar.stream.write('baz'); | ||
log.streams.foobar.stream.end(); | ||
setTimeout(function(){ | ||
tmp.assertExists('./foo.txt'); | ||
tmp.clear(); | ||
done(); | ||
}, 1000); | ||
it('Should log appropriate objects according to log level', function(){ | ||
assert.strictEqual(log.debug().level, 'debug'); | ||
assert.strictEqual(log.info().level, 'info'); | ||
assert.strictEqual(log.warn().level, 'warn'); | ||
assert.strictEqual(log.error().level, 'error'); | ||
assert.strictEqual(log.fatal().level, 'fatal'); | ||
}); | ||
it('Should remove the given stream from the logger [#removeStream]', function(){ | ||
log = new Logger(); | ||
log.addStream('foobar', stream); | ||
log.removeStream('foobar'); | ||
assert.strictEqual(typeof log.streams.foobar, 'undefined'); | ||
it('Should printf format message with input arguments', function(){ | ||
assert.strictEqual(log.debug('a b %s d %s', 'c', 'e').msg, 'a b c d e'); | ||
}); | ||
}); | ||
describe('Logging', function(){ | ||
beforeEach(function(){ | ||
log = new Logger(); | ||
log.addStream('mem', stream); | ||
it('Should assign first object argument properties to final log entry', function(){ | ||
let e = log.info({ a: 1, b: 2 }); | ||
assert.strictEqual(e.a, 1); | ||
assert.strictEqual(e.b, 2); | ||
}); | ||
it('Should log the given message as JSON', function(done){ | ||
stream.on('data', buf => { | ||
assert.strictEqual(JSON.parse(buf.toString()).msg, 'bar'); | ||
done(); | ||
}); | ||
log.warn('bar'); | ||
it('Should include entry type', function(){ | ||
assert.strictEqual(log.warn().type, 'event'); | ||
assert.strictEqual(log.error({ type: 'foobar' }).type, 'foobar'); | ||
}); | ||
it('Should log additional data in the message as JSON', function(done){ | ||
stream.on('data', buf => { | ||
let ent = JSON.parse(buf.toString()); | ||
assert.strictEqual(ent.msg, 'bar'); | ||
assert.strictEqual(ent.foo, 'baz'); | ||
done(); | ||
}); | ||
log.warn({ foo: 'baz' }, 'bar'); | ||
}); | ||
}); | ||
it('Should not log when entry level is below logger level', function(done){ | ||
stream.on('data', done); | ||
log.debug({ foo: 'baz' }, 'bar'); | ||
done(); | ||
}); | ||
describe('Auto-Parsing', function(){ | ||
it('Should broadcast log entries to all streams', function(done){ | ||
let i = 0; | ||
let test = () => i > 0 ? done() : i++; | ||
stream.on('data', test); | ||
let stream2 = new MemoryStream(); | ||
stream2.on('data', buf => { | ||
assert.strictEqual(JSON.parse(buf.toString()).msg, 'barbaz'); | ||
test(); | ||
}); | ||
log.addStream('other', stream2); | ||
log.warn('barbaz'); | ||
it('Should parse error objects', function(){ | ||
let log = new Logger(); | ||
assert(Array.isArray(log.warn({ err: new Error('Foobar') }).stack)); | ||
assert(Array.isArray(log.info({ err: 'My Error' }).stack)); | ||
}); | ||
}); | ||
const http = require('http'); | ||
describe('Formatting', function(){ | ||
beforeEach(function(){ | ||
log = new Logger(); | ||
log.addStream('mem', stream); | ||
it('Should parse http REQuest objects', function(){ | ||
let log = new Logger(); | ||
let req = http.request('http://example.com'); | ||
req.on('error', Function.prototype); | ||
assert.strictEqual(log.warn({ req }).method, 'GET'); | ||
req.destroy(); | ||
}); | ||
it('Should format message with input variables', function(done){ | ||
stream.on('data', buf => { | ||
assert.strictEqual(JSON.parse(buf.toString()).msg, 'bar 1 cool'); | ||
it('Should parse http RESponse objects', function(done){ | ||
let log = new Logger(); | ||
let req = http.request('http://example.com'); | ||
req.on('response', res => { | ||
let e = log.info({ res }); | ||
assert.strictEqual(e.method, 'GET'); | ||
assert.strictEqual(e.status, 200); | ||
assert.strictEqual(e.type, 'response'); | ||
done(); | ||
}); | ||
log.warn('bar %d %s', 1, 'cool'); | ||
req.end(); | ||
}); | ||
it('Should format error objects in the input data', function(done){ | ||
stream.on('data', buf => { | ||
let out = JSON.parse(buf.toString()); | ||
assert.strictEqual(out.err.name, 'Error'); | ||
assert.strictEqual(out.msg, 'foo'); | ||
it('Should force level warn on 5xx response', function(done){ | ||
let log = new Logger(); | ||
let req = http.request('http://httpbin.org/status/500'); | ||
req.on('response', res => { | ||
let e = log.info({ res }); | ||
assert.strictEqual(e.status, 500); | ||
assert.strictEqual(e.level, 'warn'); | ||
done(); | ||
}); | ||
log.warn({ err: new Error() }, 'foo'); | ||
req.end(); | ||
}); | ||
it('Should format http request objects in the input data', function(done){ | ||
const http = require('http'); | ||
const server = http.createServer(function(req, res){ | ||
res.end(); | ||
log.warn({ req: req }, 'foo'); | ||
}); | ||
stream.on('data', buf => { | ||
let out = JSON.parse(buf.toString()); | ||
assert.strictEqual(out.req.host, 'localhost:8765'); | ||
assert.strictEqual(out.msg, 'foo'); | ||
server.close(); | ||
done(); | ||
}); | ||
server.listen(8765); | ||
http.get('http://localhost:8765'); | ||
it('Should execute added parsers', function(){ | ||
let log = new Logger(); | ||
log.addParser(() => ({ foobar: true })); | ||
let e = log.info(); | ||
assert(e.foobar); | ||
}); | ||
@@ -140,83 +92,25 @@ | ||
beforeEach(function(){ | ||
log = new Logger(); | ||
log.addStream('mem', stream); | ||
it('Should not log when entry level is below conf level [level]', function(){ | ||
let log = new Logger({ level: 'error' }); | ||
assert(!log.warn()); | ||
}); | ||
it('Should log only essential data when in minimal mode [mode = minimal]', function(done){ | ||
stream.on('data', buf => { | ||
let ent = buf.toString(); | ||
assert(/\ \-\ warn\ \-\ bar/g.test(ent)); | ||
done(); | ||
}); | ||
log.mode = 'minimal'; | ||
log.warn({ foo: 'baz' }, 'bar'); | ||
it('Should not log anything when conf is FALSE', function(){ | ||
let log = new Logger(false); | ||
assert(!log.debug()); | ||
}); | ||
it('Should log readable JSON when in pretty mode [mode = pretty]', function(done){ | ||
stream.on('data', buf => { | ||
let ent = buf.toString(); | ||
assert(/"level": "warn",[\n\r]+ "msg": "bar"/g.test(ent)); | ||
done(); | ||
}); | ||
log.mode = 'pretty'; | ||
log.warn({ foo: 'baz' }, 'bar'); | ||
it('Should only log selected types [only]', function(){ | ||
let log = new Logger({ only: 'a' }); | ||
assert(log.debug({ type: 'a' })); | ||
assert(!log.debug({ type: 'b' })); | ||
}); | ||
it('Should not log when entry level is below stream level [stream.level]', function(done){ | ||
log.streams.mem.level = 'error'; | ||
stream.on('data', done); | ||
log.on('warn', () => done()); | ||
log.warn({ foo: 'baz' }, 'bar'); | ||
it('Should not log filtered types [except]', function(){ | ||
let log = new Logger({ except: [ 'a', 'c' ] }); | ||
assert(!log.debug({ type: 'a' })); | ||
assert(log.debug({ type: 'b' })); | ||
assert(!log.debug({ type: 'c' })); | ||
}); | ||
it('Should add a single main file stream [file]', function(done){ | ||
let tmp = new Tempper(); | ||
log = new Logger({ file: './abc.txt' }); | ||
assert.strictEqual(typeof log.streams.main, 'object'); | ||
log.streams.main.stream.end(); | ||
setTimeout(function(){ | ||
tmp.assertExists('./abc.txt'); | ||
tmp.clear(); | ||
done(); | ||
}, 1400); | ||
}); | ||
it('Should add a single main stream [stream]', function(done){ | ||
log = new Logger({ stream }); | ||
stream.on('data', buf => { | ||
assert.strictEqual(JSON.parse(buf.toString()).msg, 'bar'); | ||
done(); | ||
}); | ||
log.warn('bar'); | ||
}); | ||
}); | ||
describe('Regression', function(){ | ||
beforeEach(function(){ | ||
log = new Logger(); | ||
log.addStream('mem', stream); | ||
}); | ||
it('Should not throw exception when error event not handled', function(){ | ||
log.error('bar'); | ||
}); | ||
it('Should always append to files', function(done){ | ||
let tmp = new Tempper(); | ||
fs.writeFileSync('./test', 'abc'); | ||
log = new Logger({ file: './test' }); | ||
log.mode = 'minimal'; | ||
log.warn('hey'); | ||
log.streams.main.stream.end(); | ||
setTimeout(function(){ | ||
let data = fs.readFileSync('./test'); | ||
assert(/^abc.*\- warn \- hey/g.test(data.toString())); | ||
tmp.clear(); | ||
done(); | ||
}, 1200); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
0
15621
194
112
2