Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,11 @@ console.dir(willNeverThrow.transform({

### Align

The `align` format adds a `\t` delimiter before the message to align it in the same place.
The `align` format prepends a `\t` delimiter to `info.message`; it does not pad
`info.level`. Alignment depends on the text preceding that tab in the final output
and on the viewer's tab stops. For example, `combine(align(), simple())` produces
`info: \tmessage` and `verbose: \tmessage`, which reach different tab stops in a
viewer with eight-column tabs.

```js
const { format } = require('logform');
Expand All @@ -233,6 +237,41 @@ console.log(info);

This was previously exposed as `{ align: true }` in `winston < 3.0.0`.

To align messages across different level lengths with `simple`, use `padLevels`
instead of `align`. It adds spaces based on the configured level names, so it does
not depend on tab stops:

```js
const { format } = require('logform');
const LEVEL = Symbol.for('level');
const MESSAGE = Symbol.for('message');

const alignedSimple = format.combine(
format.padLevels(),
format.simple()
);

for (const level of ['info', 'verbose']) {
const info = alignedSimple.transform({
level,
[LEVEL]: level,
message: 'my message'
});
console.log(info[MESSAGE]);
}
// info: my message
// verbose: my message
```

`padLevels()` defaults to npm levels. For custom levels, pass the complete level
map as `padLevels({ levels })`. Padding uses `info[Symbol.for('level')]`, which
Winston supplies; set it when calling transforms directly, as above. Put
`padLevels` before the finalizing `simple` or `printf` format. With `printf`, keep
the other prefix fields at a fixed width for each level. Padding is based on level
string lengths, not terminal display widths for wide or combining characters.
`cli({ levels })` also combines padding and colorization, but unlike `simple` it
does not append extra metadata to the output.

### CLI

The `cli` format is a combination of the `colorize` and the `padLevels` formats. It turns a log `info` object into the same format previously available in `winston.cli()` in `winston < 3.0.0`.
Expand Down
53 changes: 53 additions & 0 deletions test/align.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

const assume = require('assume');
const align = require('../align');
const { LEVEL, MESSAGE } = require('triple-beam');

// Model a viewer's tab stops for ASCII text, using zero-based columns.
function messageColumn(line, tabWidth) {
return Array.from(line.slice(0, line.indexOf('message'))).reduce(
(column, char) => char === '\t'
? column + tabWidth - column % tabWidth
: column + 1,
0
);
}

describe('align', () => {
it('prepends a tab without changing the level or metadata', () => {
const fmt = align();
const info = { level: 'info', [LEVEL]: 'info', message: 'message', requestId: 42 };
const actual = fmt.transform(info, fmt.options);

assume(actual).equals(info);
assume(actual.message).equals('\tmessage');
assume(actual.level).equals('info');
assume(actual[LEVEL]).equals('info');
assume(actual.requestId).equals(42);
});

it('coerces non-string messages using template interpolation', () => {
const fmt = align();
[42, null, undefined, { key: 'value' }].forEach(message => {
const actual = fmt.transform({ level: 'info', message }, fmt.options);
assume(actual.message).equals(`\t${message}`);
});
});

it('depends on the prefix and viewer tab width when combined with simple', () => {
const { format } = require('../');
const fmt = format.combine(align(), format.simple());
const lines = ['info', 'verbose', 'really-really-long'].map(level => {
return fmt.transform({ level, [LEVEL]: level, message: 'message' })[MESSAGE];
});

assume(lines).deep.equals([
'info: \tmessage',
'verbose: \tmessage',
'really-really-long: \tmessage'
]);
assume(lines.map(line => messageColumn(line, 4))).deep.equals([8, 12, 24]);
assume(lines.map(line => messageColumn(line, 8))).deep.equals([8, 16, 24]);
});
});
82 changes: 82 additions & 0 deletions test/combine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const combine = require('../combine');
const label = require('../label');
const timestamp = require('../timestamp');
const { formats } = require('./helpers');
const { format } = require('../');
const { LEVEL, MESSAGE } = require('triple-beam');
const colors = require('@colors/colors/safe');

describe('combine', () => {
it('exposes the cascade function', () => {
Expand Down Expand Up @@ -43,6 +46,85 @@ describe('combine', () => {
});
});

describe('level padding compositions', () => {
const levels = { 'info': 0, 'verbose': 1, 'really-really-long': 2 };
const levelColors = { 'info': 'blue', 'verbose': 'cyan', 'really-really-long': 'yellow' };
const { Colorizer } = require('../colorize');
let originalColors;

beforeEach(() => {
originalColors = Colorizer.allColors;
});

afterEach(() => {
Colorizer.allColors = originalColors;
});

function assertAligned(fmt, column, metadata = '') {
Object.keys(levels).forEach(level => {
const actual = fmt.transform({
level,
[LEVEL]: level,
message: 'message',
requestId: 42
});
const output = colors.stripColors(actual[MESSAGE]);

assume(output.indexOf('message')).equals(column);
assume(output.slice(column)).equals(`message${metadata}`);
assume(actual[LEVEL]).equals(level);
assume(colors.stripColors(actual.level)).equals(level);
assume(actual.requestId).equals(42);
});
}

it('aligns the README simple example using the default npm levels', () => {
const fmt = combine(format.padLevels(), format.simple());
const output = ['info', 'verbose'].map(level => {
return fmt.transform({ level, [LEVEL]: level, message: 'my message' })[MESSAGE];
});

assume(output).deep.equals(['info: my message', 'verbose: my message']);
});

it('aligns custom levels with simple while retaining metadata', () => {
assertAligned(combine(format.padLevels({ levels }), format.simple()),
21, ' {"requestId":42}');
});

it('uses the raw LEVEL symbol when padding colorized levels', () => {
assertAligned(combine(
format.colorize({ colors: levelColors }),
format.padLevels({ levels }),
format.simple()
), 21, ' {"requestId":42}');
});

it('preserves alignment when coloring both the level and padded message', () => {
assertAligned(combine(
format.padLevels({ levels }),
format.colorize({ colors: levelColors, all: true }),
format.simple()
), 21, ' {"requestId":42}');
});

it('preserves alignment when removing colors before simple', () => {
assertAligned(combine(
format.colorize({ colors: levelColors }),
format.padLevels({ levels }),
format.uncolorize(),
format.simple()
), 21, ' {"requestId":42}');
});

it('aligns a printf layout with a fixed-width prefix', () => {
assertAligned(combine(
format.padLevels({ levels }),
format.printf(info => `[app] ${info.level}:${info.message}`)
), 26);
});
});

describe('.transform(info, opts)', () => {
it('invokes all intermediary formats', () => {
const labelTimestamp = combine(
Expand Down