Skip to content

Commit

Permalink
feat: separate built-in formatters into separate files
Browse files Browse the repository at this point in the history
  • Loading branch information
KyleRoss committed Dec 10, 2021
1 parent fddc993 commit f8ecac7
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 0 deletions.
64 changes: 64 additions & 0 deletions src/formatters/full.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import 'expect-more-jest';
import stringify from 'fast-safe-stringify';
import LogMessage from '../LogMessage';
import fullFormatter from './full';

const defaultOpts = {
meta: {},
dynamicMeta: null,
tags: [],
levelKey: '_logLevel',
messageKey: 'msg',
tagsKey: '_tags',
replacer: null
};

describe('formatters/full', () => {
it('should export a closure function', () => {
expect(typeof fullFormatter).toBe('function');
});

it('should return a formatter function', () => {
expect(typeof fullFormatter()).toBe('function');
});

it('should overrride configuration', () => {
const formatter = fullFormatter({
includeTimestamp: false,
includeTags: false,
includeMeta: false,
separator: '\t',
inspectOptions: {
colors: false,
maxArrayLength: 25
}
});

const cfg = formatter._cfg!;

expect(cfg.includeTimestamp).toBe(false);
expect(cfg.includeTags).toBe(false);
expect(cfg.includeMeta).toBe(false);
expect(cfg.separator).toBe('\t');
expect(cfg.inspectOptions).toEqual({
depth: Infinity,
colors: false,
maxArrayLength: 25
});
});

it('should skip the timestamp if includeTimestamp is false', () => {
const msg = new LogMessage({
level: 'info',
msg: 'info test',
meta: {},
tags: []
}, defaultOpts);

const formatter = fullFormatter({
includeTimestamp: false
});

expect(formatter(msg, defaultOpts, stringify)).toMatch(/^INFO\tinfo test$/);
});
});
62 changes: 62 additions & 0 deletions src/formatters/full.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { FormatPlugin } from '../typings.js';
import { inspect, InspectOptions } from 'util';

type FullFormatterCfg = {
includeTimestamp?: boolean;
formatTimestamp?: (timestamp: Date) => string;
includeTags?: boolean;
includeMeta?: boolean;
separator?: string;
inspectOptions?: InspectOptions;
};

/**
* Full formatter for log messages.
* @param {object} cfg Configuration object for the formatter.
* @returns {FormatPlugin} The full formatter function.
*/
export default function fullFormatter(cfg: FullFormatterCfg = {}): FormatPlugin {
const fmCfg = {
includeTimestamp: true,
formatTimestamp: (timestamp: Date) => timestamp.toISOString(),
includeTags: true,
includeMeta: true,
separator: '\t',
...cfg
};

fmCfg.inspectOptions = {
depth: Infinity,
colors: true,
...(fmCfg.inspectOptions ?? {})
};

const fullFmt: FormatPlugin = (ctx): string => {
const msg = [];
if(fmCfg.includeTimestamp) {
msg.push(fmCfg.formatTimestamp(new Date()));
}

msg.push(ctx.level.toUpperCase(), ctx.msg);

const parts = [
msg.join(fmCfg.separator)
];

if(fmCfg.includeTags && ctx.tags.length) {
const tags = ctx.tags.map(tag => `${tag}`).join(', ');
parts.push(`→ ${tags}`);
}

if(fmCfg.includeMeta && Object.keys(ctx.meta).length) {
const meta = inspect(ctx.meta, fmCfg.inspectOptions!);
parts.push(`→ ${meta.replace(/\n/g, '\n ')}`);
}

return parts.join('\n');
};

fullFmt._cfg = fmCfg;

return fullFmt;
}
3 changes: 3 additions & 0 deletions src/formatters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as json } from './json.js';
export { default as full } from './full.js';
export { default as minimal } from './minimal.js';
81 changes: 81 additions & 0 deletions src/formatters/json.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'expect-more-jest';
import stringify from 'fast-safe-stringify';
import LogMessage from '../LogMessage';
import jsonFormatter from './json';

const defaultOpts = {
meta: {},
dynamicMeta: null,
tags: [],
levelKey: '_logLevel',
messageKey: 'msg',
tagsKey: '_tags',
replacer: null
};

const logObject = {
level: 'info',
msg: 'info test',
meta: {},
tags: []
};


describe('formatters/json', () => {
it('should export a closure function', () => {
expect(typeof jsonFormatter).toBe('function');
});

it('should return a formatter function', () => {
expect(typeof jsonFormatter()).toBe('function');
});

const replacerOpts = {
...defaultOpts,
meta: { ssn: '444-55-6666' },
replacer(key: string, value: unknown) {
if(key === 'ssn') {
return `${(value as string).substring(0, 3)}-**-****`;
}

return value;
}
};

const msg = new LogMessage({ ...logObject }, replacerOpts);

const formatter = jsonFormatter();
const result = formatter(msg, replacerOpts, stringify);

it('should return log in JSON format', () => {
expect(result).toBeJsonString();
});

it('should run replacer function', () => {
expect(JSON.parse(result).ssn).toBe('444-**-****');
});

it('should not run replacer function when not defined', () => {
const msgNoReplacer = new LogMessage({ ...logObject }, {
...defaultOpts,
meta: { ssn: '444-55-6666' }
});

const noReplacerResult = formatter(msgNoReplacer, defaultOpts, stringify);

expect(JSON.parse(noReplacerResult).ssn).toBe('444-55-6666');
});

it('should pretty print JSON when dev is "true"', () => {
const opts = {
...defaultOpts,
dev: true,
meta: { ssn: '444-55-6666' }
};

const msgDev = new LogMessage({ ...logObject }, opts);
const prettyResult = formatter(msgDev, opts, stringify);

expect(/\n/g.test(prettyResult)).toBe(true);
});
});
12 changes: 12 additions & 0 deletions src/formatters/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FormatPlugin } from '../typings.js';

/**
* JSON formatter for log messages.
* @returns {FormatPlugin} The JSON formatter function.
*/
export default function jsonFormatter(): FormatPlugin {
const jsonFmt: FormatPlugin = (ctx, options, stringify): string =>
stringify(ctx.value, options.replacer ?? undefined, options.dev ? 2 : 0);

return jsonFmt;
}
73 changes: 73 additions & 0 deletions src/formatters/minimal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'expect-more-jest';
import stringify from 'fast-safe-stringify';
import LogMessage from '../LogMessage';
import minimalFormatter from './minimal';

const defaultOpts = {
meta: {},
dynamicMeta: null,
tags: [],
levelKey: '_logLevel',
messageKey: 'msg',
tagsKey: '_tags',
replacer: null
};

describe('formatters/minmal', () => {
it('should export a closure function', () => {
expect(typeof minimalFormatter).toBe('function');
});

it('should return a formatter function', () => {
expect(typeof minimalFormatter()).toBe('function');
});

it('should overrride configuration', () => {
const formatter = minimalFormatter({
includeTimestamp: false,
separator: '\t'
});

const cfg = formatter._cfg!;

expect(cfg.includeTimestamp).toBe(false);
expect(cfg.separator).toBe('\t');
});

it('should format timestamp as ISO string by default', () => {
const formatter = minimalFormatter();

// @ts-expect-error - we're testing the internals here
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)$/);
});

it('should include the timestamp if includeTimestamp is true', () => {
const msg = new LogMessage({
level: 'info',
msg: 'info test',
meta: {},
tags: []
}, defaultOpts);

const formatter = minimalFormatter({
includeTimestamp: true
});

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$/);
});

it('should skip the timestamp if includeTimestamp is false', () => {
const msg = new LogMessage({
level: 'info',
msg: 'info test',
meta: {},
tags: []
}, defaultOpts);

const formatter = minimalFormatter({
includeTimestamp: false
});

expect(formatter(msg, defaultOpts, stringify)).toMatch(/^INFO | info test$/);
});
});
36 changes: 36 additions & 0 deletions src/formatters/minimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FormatPlugin } from '../typings.js';

type MinimalFormatterCfg = {
includeTimestamp?: boolean;
formatTimestamp?: (timestamp: Date) => string;
separator?: string;
};

/**
* Minimal formatter for log messages.
* @param {object} cfg Configuration object for the formatter.
* @returns {FormatPlugin} The minimal formatter function.
*/
export default function minimalFormatter(cfg: MinimalFormatterCfg = {}): FormatPlugin {
const fmCfg = {
includeTimestamp: false,
formatTimestamp: (timestamp: Date) => timestamp.toISOString(),
separator: ' | ',
...cfg
};

const minimalFmt: FormatPlugin = (ctx): string => {
const parts = [];
if(fmCfg.includeTimestamp) {
parts.push(fmCfg.formatTimestamp(new Date()));
}

parts.push(ctx.level.toUpperCase(), ctx.msg);

return parts.join(fmCfg.separator);
};

minimalFmt._cfg = fmCfg;

return minimalFmt;
}

0 comments on commit f8ecac7

Please sign in to comment.