Skip to content

Commit f8ecac7

Browse files
committed
feat: separate built-in formatters into separate files
1 parent fddc993 commit f8ecac7

File tree

7 files changed

+331
-0
lines changed

7 files changed

+331
-0
lines changed

src/formatters/full.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'expect-more-jest';
2+
import stringify from 'fast-safe-stringify';
3+
import LogMessage from '../LogMessage';
4+
import fullFormatter from './full';
5+
6+
const defaultOpts = {
7+
meta: {},
8+
dynamicMeta: null,
9+
tags: [],
10+
levelKey: '_logLevel',
11+
messageKey: 'msg',
12+
tagsKey: '_tags',
13+
replacer: null
14+
};
15+
16+
describe('formatters/full', () => {
17+
it('should export a closure function', () => {
18+
expect(typeof fullFormatter).toBe('function');
19+
});
20+
21+
it('should return a formatter function', () => {
22+
expect(typeof fullFormatter()).toBe('function');
23+
});
24+
25+
it('should overrride configuration', () => {
26+
const formatter = fullFormatter({
27+
includeTimestamp: false,
28+
includeTags: false,
29+
includeMeta: false,
30+
separator: '\t',
31+
inspectOptions: {
32+
colors: false,
33+
maxArrayLength: 25
34+
}
35+
});
36+
37+
const cfg = formatter._cfg!;
38+
39+
expect(cfg.includeTimestamp).toBe(false);
40+
expect(cfg.includeTags).toBe(false);
41+
expect(cfg.includeMeta).toBe(false);
42+
expect(cfg.separator).toBe('\t');
43+
expect(cfg.inspectOptions).toEqual({
44+
depth: Infinity,
45+
colors: false,
46+
maxArrayLength: 25
47+
});
48+
});
49+
50+
it('should skip the timestamp if includeTimestamp is false', () => {
51+
const msg = new LogMessage({
52+
level: 'info',
53+
msg: 'info test',
54+
meta: {},
55+
tags: []
56+
}, defaultOpts);
57+
58+
const formatter = fullFormatter({
59+
includeTimestamp: false
60+
});
61+
62+
expect(formatter(msg, defaultOpts, stringify)).toMatch(/^INFO\tinfo test$/);
63+
});
64+
});

src/formatters/full.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { FormatPlugin } from '../typings.js';
2+
import { inspect, InspectOptions } from 'util';
3+
4+
type FullFormatterCfg = {
5+
includeTimestamp?: boolean;
6+
formatTimestamp?: (timestamp: Date) => string;
7+
includeTags?: boolean;
8+
includeMeta?: boolean;
9+
separator?: string;
10+
inspectOptions?: InspectOptions;
11+
};
12+
13+
/**
14+
* Full formatter for log messages.
15+
* @param {object} cfg Configuration object for the formatter.
16+
* @returns {FormatPlugin} The full formatter function.
17+
*/
18+
export default function fullFormatter(cfg: FullFormatterCfg = {}): FormatPlugin {
19+
const fmCfg = {
20+
includeTimestamp: true,
21+
formatTimestamp: (timestamp: Date) => timestamp.toISOString(),
22+
includeTags: true,
23+
includeMeta: true,
24+
separator: '\t',
25+
...cfg
26+
};
27+
28+
fmCfg.inspectOptions = {
29+
depth: Infinity,
30+
colors: true,
31+
...(fmCfg.inspectOptions ?? {})
32+
};
33+
34+
const fullFmt: FormatPlugin = (ctx): string => {
35+
const msg = [];
36+
if(fmCfg.includeTimestamp) {
37+
msg.push(fmCfg.formatTimestamp(new Date()));
38+
}
39+
40+
msg.push(ctx.level.toUpperCase(), ctx.msg);
41+
42+
const parts = [
43+
msg.join(fmCfg.separator)
44+
];
45+
46+
if(fmCfg.includeTags && ctx.tags.length) {
47+
const tags = ctx.tags.map(tag => `${tag}`).join(', ');
48+
parts.push(`→ ${tags}`);
49+
}
50+
51+
if(fmCfg.includeMeta && Object.keys(ctx.meta).length) {
52+
const meta = inspect(ctx.meta, fmCfg.inspectOptions!);
53+
parts.push(`→ ${meta.replace(/\n/g, '\n ')}`);
54+
}
55+
56+
return parts.join('\n');
57+
};
58+
59+
fullFmt._cfg = fmCfg;
60+
61+
return fullFmt;
62+
}

src/formatters/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as json } from './json.js';
2+
export { default as full } from './full.js';
3+
export { default as minimal } from './minimal.js';

src/formatters/json.spec.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import 'expect-more-jest';
2+
import stringify from 'fast-safe-stringify';
3+
import LogMessage from '../LogMessage';
4+
import jsonFormatter from './json';
5+
6+
const defaultOpts = {
7+
meta: {},
8+
dynamicMeta: null,
9+
tags: [],
10+
levelKey: '_logLevel',
11+
messageKey: 'msg',
12+
tagsKey: '_tags',
13+
replacer: null
14+
};
15+
16+
const logObject = {
17+
level: 'info',
18+
msg: 'info test',
19+
meta: {},
20+
tags: []
21+
};
22+
23+
24+
describe('formatters/json', () => {
25+
it('should export a closure function', () => {
26+
expect(typeof jsonFormatter).toBe('function');
27+
});
28+
29+
it('should return a formatter function', () => {
30+
expect(typeof jsonFormatter()).toBe('function');
31+
});
32+
33+
const replacerOpts = {
34+
...defaultOpts,
35+
meta: { ssn: '444-55-6666' },
36+
replacer(key: string, value: unknown) {
37+
if(key === 'ssn') {
38+
return `${(value as string).substring(0, 3)}-**-****`;
39+
}
40+
41+
return value;
42+
}
43+
};
44+
45+
const msg = new LogMessage({ ...logObject }, replacerOpts);
46+
47+
const formatter = jsonFormatter();
48+
const result = formatter(msg, replacerOpts, stringify);
49+
50+
it('should return log in JSON format', () => {
51+
expect(result).toBeJsonString();
52+
});
53+
54+
it('should run replacer function', () => {
55+
expect(JSON.parse(result).ssn).toBe('444-**-****');
56+
});
57+
58+
it('should not run replacer function when not defined', () => {
59+
const msgNoReplacer = new LogMessage({ ...logObject }, {
60+
...defaultOpts,
61+
meta: { ssn: '444-55-6666' }
62+
});
63+
64+
const noReplacerResult = formatter(msgNoReplacer, defaultOpts, stringify);
65+
66+
expect(JSON.parse(noReplacerResult).ssn).toBe('444-55-6666');
67+
});
68+
69+
it('should pretty print JSON when dev is "true"', () => {
70+
const opts = {
71+
...defaultOpts,
72+
dev: true,
73+
meta: { ssn: '444-55-6666' }
74+
};
75+
76+
const msgDev = new LogMessage({ ...logObject }, opts);
77+
const prettyResult = formatter(msgDev, opts, stringify);
78+
79+
expect(/\n/g.test(prettyResult)).toBe(true);
80+
});
81+
});

src/formatters/json.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FormatPlugin } from '../typings.js';
2+
3+
/**
4+
* JSON formatter for log messages.
5+
* @returns {FormatPlugin} The JSON formatter function.
6+
*/
7+
export default function jsonFormatter(): FormatPlugin {
8+
const jsonFmt: FormatPlugin = (ctx, options, stringify): string =>
9+
stringify(ctx.value, options.replacer ?? undefined, options.dev ? 2 : 0);
10+
11+
return jsonFmt;
12+
}

src/formatters/minimal.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'expect-more-jest';
2+
import stringify from 'fast-safe-stringify';
3+
import LogMessage from '../LogMessage';
4+
import minimalFormatter from './minimal';
5+
6+
const defaultOpts = {
7+
meta: {},
8+
dynamicMeta: null,
9+
tags: [],
10+
levelKey: '_logLevel',
11+
messageKey: 'msg',
12+
tagsKey: '_tags',
13+
replacer: null
14+
};
15+
16+
describe('formatters/minmal', () => {
17+
it('should export a closure function', () => {
18+
expect(typeof minimalFormatter).toBe('function');
19+
});
20+
21+
it('should return a formatter function', () => {
22+
expect(typeof minimalFormatter()).toBe('function');
23+
});
24+
25+
it('should overrride configuration', () => {
26+
const formatter = minimalFormatter({
27+
includeTimestamp: false,
28+
separator: '\t'
29+
});
30+
31+
const cfg = formatter._cfg!;
32+
33+
expect(cfg.includeTimestamp).toBe(false);
34+
expect(cfg.separator).toBe('\t');
35+
});
36+
37+
it('should format timestamp as ISO string by default', () => {
38+
const formatter = minimalFormatter();
39+
40+
// @ts-expect-error - we're testing the internals here
41+
expect((formatter._cfg!).formatTimestamp(new Date())).toMatch(/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/);
42+
});
43+
44+
it('should include the timestamp if includeTimestamp is true', () => {
45+
const msg = new LogMessage({
46+
level: 'info',
47+
msg: 'info test',
48+
meta: {},
49+
tags: []
50+
}, defaultOpts);
51+
52+
const formatter = minimalFormatter({
53+
includeTimestamp: true
54+
});
55+
56+
expect(formatter(msg, defaultOpts, stringify)).toMatch(/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z) | INFO | info test$/);
57+
});
58+
59+
it('should skip the timestamp if includeTimestamp is false', () => {
60+
const msg = new LogMessage({
61+
level: 'info',
62+
msg: 'info test',
63+
meta: {},
64+
tags: []
65+
}, defaultOpts);
66+
67+
const formatter = minimalFormatter({
68+
includeTimestamp: false
69+
});
70+
71+
expect(formatter(msg, defaultOpts, stringify)).toMatch(/^INFO | info test$/);
72+
});
73+
});

src/formatters/minimal.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { FormatPlugin } from '../typings.js';
2+
3+
type MinimalFormatterCfg = {
4+
includeTimestamp?: boolean;
5+
formatTimestamp?: (timestamp: Date) => string;
6+
separator?: string;
7+
};
8+
9+
/**
10+
* Minimal formatter for log messages.
11+
* @param {object} cfg Configuration object for the formatter.
12+
* @returns {FormatPlugin} The minimal formatter function.
13+
*/
14+
export default function minimalFormatter(cfg: MinimalFormatterCfg = {}): FormatPlugin {
15+
const fmCfg = {
16+
includeTimestamp: false,
17+
formatTimestamp: (timestamp: Date) => timestamp.toISOString(),
18+
separator: ' | ',
19+
...cfg
20+
};
21+
22+
const minimalFmt: FormatPlugin = (ctx): string => {
23+
const parts = [];
24+
if(fmCfg.includeTimestamp) {
25+
parts.push(fmCfg.formatTimestamp(new Date()));
26+
}
27+
28+
parts.push(ctx.level.toUpperCase(), ctx.msg);
29+
30+
return parts.join(fmCfg.separator);
31+
};
32+
33+
minimalFmt._cfg = fmCfg;
34+
35+
return minimalFmt;
36+
}

0 commit comments

Comments
 (0)