-
Notifications
You must be signed in to change notification settings - Fork 150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for messageFormat strings containing conditionals #442
Changes from 4 commits
39feab1
2acc0fe
78578c5
b6894c6
8f22437
25eab67
44ac4c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -42,7 +42,8 @@ module.exports.internals = { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
deleteLogProperty, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
splitPropertyKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
createDate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
isValidDate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
isValidDate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
parseMessageFormat | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -263,16 +264,21 @@ function prettifyLevel ({ log, colorizer = defaultColorizer, levelKey = LEVEL_KE | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function prettifyMessage ({ log, messageFormat, messageKey = MESSAGE_KEY, colorizer = defaultColorizer, levelLabel = LEVEL_LABEL, levelKey = LEVEL_KEY, customLevels, useOnlyCustomProps }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (messageFormat && typeof messageFormat === 'string') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const message = String(messageFormat).replace(/{([^{}]+)}/g, function (match, p1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// return log level as string instead of int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let level | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (p1 === levelLabel && (level = getPropertyValue(log, levelKey)) !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const condition = useOnlyCustomProps ? customLevels === undefined : customLevels[level] === undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return condition ? LEVELS[level] : customLevels[level] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Parse nested key access, e.g. `{keyA.subKeyB}`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return getPropertyValue(log, p1) || '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const parsedMessageFormat = parseMessageFormat(messageFormat, log) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const message = String(parsedMessageFormat).replace( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/{([^{}]+)}/g, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function (match, p1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// return log level as string instead of int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let level | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (p1 === levelLabel && (level = getPropertyValue(log, levelKey)) !== undefined) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const condition = useOnlyCustomProps ? customLevels === undefined : customLevels[level] === undefined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return condition ? LEVELS[level] : customLevels[level] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Parse nested key access, e.g. `{keyA.subKeyB}`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return getPropertyValue(log, p1) || '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return colorizer.message(message) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (messageFormat && typeof messageFormat === 'function') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -383,7 +389,7 @@ function prettifyObject ({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Split object keys into two categories: error and non-error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { plain, errors } = Object.entries(input).reduce(({ plain, errors }, [k, v]) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (keysToIgnore.includes(k) === false) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Pre-apply custom prettifiers, because all 3 cases below will need this | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Pre-apply custom prettifiers, because all 3 cases below will need this | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const pretty = typeof customPrettifiers[k] === 'function' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
? customPrettifiers[k](v, k, input) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
: v | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -557,7 +563,7 @@ function splitPropertyKey (key) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @param {object} obj The object to be searched. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @param {string|string[]} property A string, or an array of strings, identifying | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* the property to be retrieved from the object. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* the property to be retrieved from the object. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Accepts nested properties delimited by a `.`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Delimiter can be escaped to preserve property names that contain the delimiter. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -766,3 +772,32 @@ function handleCustomlevelNamesOpts (cLevels) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* Translates all conditional blocks from within the messageFormat. Translates any matching | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* {if key}{key}{end} statements and returns everything between if and else blocks if the key provided | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* was found in log. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @param {string} messageFormat A format string or function that defines how the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* logged message should be conditionally formatted, e.g. `'{if level}{level}{end} - {if req.id}{req.id}{end}'`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @param {object} log The log object to be modified. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
* @returns {string} The parsed messageFormat. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
function parseMessageFormat (messageFormat, log) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
messageFormat = messageFormat.replace(/{if (.*?)}(.*?){end}/g, function (_, key, value) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const propertyValue = getPropertyValue(log, key) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (propertyValue && value.includes(key)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return value.replace(new RegExp('{' + key + '}', 'g'), propertyValue) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Remove unescaped if blocks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
messageFormat = messageFormat.replace(/{if (.*?)}/g, '') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Remove unescaped end blocks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
messageFormat = messageFormat.replace(/{end}/g, '') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
timlohse1104 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return messageFormat.replace(/\s+/g, ' ').trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would be a bit easier to read:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated accordingly. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -222,3 +222,65 @@ tap.test('#getPropertyValue', t => { | |
|
||
t.end() | ||
}) | ||
|
||
tap.test('#parseMessageFormat', t => { | ||
const logData = { | ||
level: 30, | ||
data1: { | ||
data2: 'bar' | ||
}, | ||
msg: 'foo' | ||
} | ||
|
||
t.test('parseMessageFormat translates if / else statement to found property value', async t => { | ||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level} - {if data1.data2}{data1.data2}{end}', log), '{level} - bar') | ||
}) | ||
|
||
t.test('parseMessageFormat translates if / else statement to found property value and leave unmatched property key untouched', async t => { | ||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level} - {if data1.data2}{data1.data2} ({msg}){end}', log), '{level} - bar ({msg})') | ||
}) | ||
|
||
t.test('parseMessageFormat removes unescaped if statements', async t => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe you mean non-terminated instead of unescaped? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed wording. |
||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level} - {if data1.data2}{data1.data2}', log), '{level} - {data1.data2}') | ||
}) | ||
|
||
t.test('parseMessageFormat removes unescaped end statements', async t => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think "floating" is a better term than "unescaped" here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed wording. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if the format string is?:
(i.e. someone tried to add an unsupported nested condition but failed to add the leading There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The additional |
||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level} - {data1.data2}{end}', log), '{level} - {data1.data2}') | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a few tests for not-matching if/else/end pair? What error would you get? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah for sure. Thanks for the fast code review. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added further test scenarios. |
||
|
||
t.test('parseMessageFormat removes if / end blocks if existent condition key does not match existent property key', async t => { | ||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level}{if msg}{data1.data2}{end}', log), '{level}') | ||
}) | ||
|
||
t.test('parseMessageFormat removes if / end blocks if non-existent condition key does not match existent property key', async t => { | ||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level}{if foo}{msg}{end}', log), '{level}') | ||
}) | ||
|
||
t.test('parseMessageFormat removes if / end blocks if existent condition key does not match non-existent property key', async t => { | ||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level}{if msg}{foo}{end}', log), '{level}') | ||
}) | ||
|
||
t.test('parseMessageFormat removes if / end blocks if non-existent condition key does not match non-existent property key', async t => { | ||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level}{if foo}{bar}{end}', log), '{level}') | ||
}) | ||
|
||
t.test('parseMessageFormat removes if / end blocks if nested condition key does not match property key', async t => { | ||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{level}{if data1.msg}{data1.data2}{end}', log), '{level}') | ||
}) | ||
|
||
t.test('parseMessageFormat removes nested if / end statement blocks', async t => { | ||
const log = fastCopy(logData) | ||
t.equal(internals.parseMessageFormat('{if msg}{if data1.data2}{msg}{data1.data2}{end}{end}', log), 'foo{data1.data2}') | ||
}) | ||
|
||
t.end() | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be a better name:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed accordingly.