Skip to content

Commit

Permalink
feat: log formatting using printf
Browse files Browse the repository at this point in the history
  • Loading branch information
pimlie committed Oct 10, 2018
1 parent 2929648 commit 2afb025
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 168 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,68 @@ Available reporters:
A reporter (Class or Object) exposes `log(logObj)` method.
To write a reporter, check implementations to get an idea.

### Formatting

The `BasicReporter` and `FancyReporter` support custom log formats.

#### Time formats

Specify a `timeFormat` option when creating the reporter. Default format is `HH:mm:ss`.

Consola uses Day.js for formatting, see [here](https://github.com/iamkun/dayjs/blob/master/docs/en/API-reference.md#list-of-all-available-formats) for a list of formatting options.

#### Log formats

The log formats can be customised with a `printf` format string. See [here](https://github.com/SheetJS/printj#printf-format-string-specification) for the used specification.

The following variables are supported:

##### `BasicReporter`
- `%1$s`: Date string as formatted by `timeFormat`
- `%2$s`: Log type, e.g. SUCCESS
- `%3$s`: Tag
- `%4$s`: Log message
- `%5$s`: Additional fields

##### `FancyReporter`
- `%1$s`: Start text color
- `%2$s`: End text color
- `%3$s`: Start additional text color
- `%4$s`: End additional text color
- `%5$s`: Start background color
- `%6$s`: End background color
- `%7$s`: Date string as formatted by `timeFormat`
- `%8$s`: Log type, e.g. SUCCESS
- `%9$s`: Tag
- `%10$s`: Log message
- `%11$s`: Additional fields
- `%12$s`: Figure icon
- `%13$s`: Length of figure icon string + 1 if icon exists, used for conditional space after icon

```js
consola = new Consola({
reporters: [
new BasicReporter({
timeFormat: 'hh:mm:ss A'
}),
new FancyReporter({
formats: {
default: '[%7$s] [%8$-7s] %9$s%10$s%11$s\n', // same format as BasicReporter
badge: '\n' +
' /================================================\\\n' +
' |%5$s' + '%12$s'.repeat(48) + '%6$s|\n' +
' |%5$s%12$s' + ' '.repeat(46) + '%12$s%6$s|\n' +
' |%5$s%12$s %8$-44s %12$s%6$s|\n' +
' |%5$s%12$s %10$-44s %12$s%6$s|\n' +
' |%5$s%12$s' + ' '.repeat(46) + '%12$s%6$s|\n' +
' |%5$s' + '%12$s'.repeat(48) + '%6$s|\n' +
' \\================================================/\n\n'
}
})
]
})
```

## Types

Types can be treated as _extended logging levels_ in Consola's world.
Expand Down
19 changes: 11 additions & 8 deletions demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,20 @@ const Consola = esm('../src')

const reporters = [
'FancyReporter',
'ShmancyReporter',
'BasicReporter',
'JSONReporter',
'WinstonReporter'
]

for (const reporter of reporters) {
const s = process.hrtime()
const consola = new Consola.Consola({
level: 5,
reporters: [new Consola[reporter]({
errStream: process.stdout
})]
})

for (let i = 0; i < 10; i++) {
for (let i = 0; i < 1; i++) {
for (let type of Object.keys(consola.types).sort()) {
consola[type](`A message with consola.${type}()`)
}
Expand All @@ -31,7 +29,7 @@ for (const reporter of reporters) {
color: '#454545'
})

if (reporter === 'FancyReporter' || reporter === 'SmancyReporter') {
if (reporter === 'FancyReporter') {
consola.success({
message: 'This is a fancy badge',
additional: 'With some additional info',
Expand All @@ -46,9 +44,14 @@ for (const reporter of reporters) {
for (let type of Object.keys(consola.types).sort()) {
tagged[type](`A tagged message with consola.${type}()`)
}
}

const t = process.hrtime(s)

console.error(`${reporter} took ${Math.round((t[0] * 1e9 + t[1]) / 1e4) / 1e2}ms`) // eslint-disable-line no-console
if (reporter === 'FancyReporter') {
tagged.success({
message: 'This is a fancy badge',
additional: 'With some additional info',
additionalColor: 'brown',
badge: true
})
}
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
],
"dependencies": {
"chalk": "^2.4.1",
"dayjs": "^1.7.7",
"figures": "^2.0.0",
"printj": "^1.2.0",
"std-env": "^2.0.2"
Expand Down
65 changes: 37 additions & 28 deletions src/reporters/basic.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import util from 'util'
import { isPlainObject, parseStack, align } from '../utils'
import { vsprintf } from 'printj'
import dayjs from 'dayjs'
import { isPlainObject, parseStack } from '../utils'

const NS_SEPARATOR = ' - '

export default class BasicReporter {
constructor (options) {
this.options = Object.assign({
stream: process.stdout,
errStream: process.stderr,
alignment: 'left',
showType: false
showType: false,
timeFormat: 'HH:mm:ss',
formats: {
/* eslint-disable no-multi-spaces */
default: '' +
'[%1$s]' + // print date in brackets
' ' +
'[%2$-7s]' + // print right padded type in brackets
' ' +
'%3$s' + // print tag
'%4$s' + // print log message
'%5$s' + // print additional arguments
'\n'
/* eslint-disable no-multi-spaces */
}
}, options)

this.write = this.write.bind(this)
Expand Down Expand Up @@ -43,7 +60,7 @@ export default class BasicReporter {
let message = logObj.message || ''
let type = logObj.type || ''
let tag = logObj.tag || ''
let date = logObj.date.toLocaleTimeString()
let date = dayjs(logObj.date).format(this.options.timeFormat)

// Format args
const args = logObj.args.map(arg => {
Expand Down Expand Up @@ -76,34 +93,26 @@ export default class BasicReporter {
}
}

log (logObj) {
const fields = this.getFields(logObj)
const write = logObj.isError ? this.writeError : this.write

// Print date
write((`[${align(this.options.alignment, fields.date, 8)}] `))
prepareWrite (logObj, fields) {
const format = this.options.formats.default

// Print type
if (fields.type.length) {
write((`[${align(this.options.alignment, fields.type.toUpperCase(), 7)}] `))
}
const argv = [
fields.date,
fields.type.toUpperCase(),
!fields.tag.length ? '' : (fields.tag.replace(/:/g, '>') + '>').split('>').join(NS_SEPARATOR),
fields.message,
!fields.args.length ? '' : '\n' + fields.args.join(' ')
]

// Print tag
if (fields.tag.length) {
write(`[${fields.tag}] `)
}
return { format, argv }
}

// Print message
if (fields.message.length) {
write(fields.message)
}
log (logObj) {
const fields = this.getFields(logObj)
const write = logObj.isError ? this.writeError : this.write

// Print additional args
if (fields.args.length) {
write('\n' + (fields.args.join(' ')))
}
const { format, argv } = this.prepareWrite(logObj, fields)

// Newline
write('\n')
write(vsprintf(format, argv))
}
}
115 changes: 81 additions & 34 deletions src/reporters/fancy.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import chalk from 'chalk'
import figures from 'figures'
import BasicReporter from './basic'
import { parseStack, align, chalkColor, chalkBgColor } from '../utils'
import { parseStack } from '../utils'

export const NS_SEPARATOR = chalkColor('blue')(figures(' › '))
function chalkColor (name) {
if (name[0] === '#') {
return chalk.hex(name)
}
return chalk[name] || chalk.keyword(name)
}

function chalkBgColor (name) {
if (name[0] === '#') {
return chalk.bgHex(name)
}
return chalk['bg' + name[0] + name.slice(1)] || chalk.bgKeyword(name)
}

const NS_SEPARATOR = chalkColor('blue')(figures(' › '))

export const ICONS = {
start: figures('●'),
Expand All @@ -18,6 +33,43 @@ export const ICONS = {
}

export default class FancyReporter extends BasicReporter {
constructor (options) {
super(Object.assign({
formats: {
/* eslint-disable no-multi-spaces */
default: '' +
'%1$s' + // use text color
'%12$*-13$s' + // print icon with right padded space if exists
'%2$s' + // end text color
'%9$s' + // print tag
'%1$s' + // use text color (tags separator reset to white)
'%10$s' + // print log message
'%2$s' + // end text color
'%3$s' + // use additional text color
'%11$s' + // print additional arguments
'%4$s' + // end additional text color
'\n',
badge: '\n' +
'%5$s' + // use background color
' %8$s ' + // log type with spacing
'%6$s' + // end background color
' ' +
'%1$s' + // use text color
'%9$s' + // print tag
'%10$s' + // print log message
'%2$s' + // end text color
'\n' +
'%3$s' + // use additional text color
'%11$s' + // print additional arguments
'%4$s' + // end additional text color
'\n\n'
/* eslint-enable no-multi-spaces */
}
}, options))

this._colorCache = {}
}

formatStack (stack) {
return ' ' + parseStack(stack).join('↲\n ')
}
Expand All @@ -26,49 +78,44 @@ export default class FancyReporter extends BasicReporter {
this.write(process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H')
}

log (logObj) {
const fields = this.getFields(logObj)
const write = logObj.isError ? this.writeError : this.write

prepareWrite (logObj, fields) {
// Clear console
if (logObj.clear) {
this.clear()
}

// Print type
const type = align(this.options.alignment, fields.type.toUpperCase(), 7)
if (logObj.badge) {
write('\n' + chalkBgColor(logObj.color).black(` ${type} `) + ' ')
} else if (fields.type !== 'log') {
const icon = logObj.icon || ICONS[fields.type] || ICONS.default
if (this.showType) {
write(chalkColor(logObj.color)(`${icon} ${type} `))
} else {
write(chalkColor(logObj.color)(`${icon} `))
const format = this.options.formats[logObj.badge ? 'badge' : 'default']

const colors = ['grey', logObj.color, logObj.additionalColor]
colors.forEach((color) => {
if (color && color.length && !this._colorCache[color]) {
this._colorCache[color] = chalkColor(color)('|').split('|')
}
}
})

// Print tag
if (fields.tag.length) {
write((fields.tag.replace(/:/g, '>') + '>').split('>').join(NS_SEPARATOR))
const bgColorKey = `bg_${logObj.color}`
if (!this._colorCache[bgColorKey]) {
this._colorCache[bgColorKey] = chalkBgColor(logObj.color).black('|').split('|')
}

// Print message
if (fields.message.length) {
write(chalkColor(logObj.color)(fields.message))
}
const argv = []
// textColor=%1,%2 additionalColor=%3,%4 and bgColor=%5,%6
Array.prototype.push.apply(argv, this._colorCache[logObj.color])
Array.prototype.push.apply(argv, this._colorCache[logObj.additionalColor || 'grey'])
Array.prototype.push.apply(argv, this._colorCache[bgColorKey])

// Badge additional line
if (logObj.badge) {
write('\n')
}
const icon = fields.type === 'log' ? '' : logObj.icon || ICONS[fields.type] || ICONS.default

// Print additional args
if (fields.args.length) {
write('\n' + chalkColor(logObj.additionalColor || 'grey')(fields.args.join(' ')))
}
Array.prototype.push.apply(argv, [
fields.date,
fields.type.toUpperCase(),
!fields.tag.length ? '' : (fields.tag.replace(/:/g, '>') + '>').split('>').join(NS_SEPARATOR),
fields.message,
!fields.args.length ? '' : '\n' + fields.args.join(' '),
icon,
icon.length ? icon.length + 1 : 0 // used for conditional spacing after icon
])

// Newline
write(logObj.badge ? '\n\n' : '\n')
return { format, argv }
}
}
1 change: 0 additions & 1 deletion src/reporters/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export { default as BasicReporter } from './basic'
export { default as BrowserReporter } from './browser'
export { default as FancyReporter } from './fancy'
export { default as ShmancyReporter } from './shmancy'
export { default as JSONReporter } from './json'
export { default as WinstonReporter } from './winston'
2 changes: 1 addition & 1 deletion src/reporters/json.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default class JSONReporter {
constructor (stream) {
constructor ({ stream }) {
this.stream = stream || process.stdout
}

Expand Down
Loading

1 comment on commit 2afb025

@pimlie
Copy link
Contributor Author

@pimlie pimlie commented on 2afb025 Oct 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pi0 Conditional formatting is a bit difficult with printf, spaces are not a problem but setting conditonal brackets around eg tag would need 2 additional argv's (one for opening bracket and one for closing). I have now implemented that the same as apache logs do by just using -, but let me know if you prefer something else.

There seems to be a length bug with printf, the example in the readme doesnt correctly pad to 46 chars:
image

Please sign in to comment.