-
Notifications
You must be signed in to change notification settings - Fork 16
feat(formatter): Add YScope formatter for structured logs and remove Logback-style formatter. #123
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
Changes from all commits
c29025d
e44d47f
71ee56c
d406e51
b55ebab
19c31d5
9ea0006
47bd40e
ebfa463
9d5ae88
c74eb59
c9a48e0
61b4769
087548d
01a3889
2acd20a
acf7a2c
197125a
cd23262
267b1f9
2385919
de03857
c6c65a1
2bba5ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import {Nullable} from "../../../../typings/common"; | ||
import {YscopeFieldFormatter} from "../../../../typings/formatters"; | ||
import {JsonValue} from "../../../../typings/js"; | ||
import {jsonValueToString} from "../utils"; | ||
|
||
|
||
/** | ||
* A field formatter that rounds numerical values to the nearest integer. | ||
* For non-numerical values, the field's value is converted to a string then returned as-is. | ||
* Options: None. | ||
*/ | ||
class RoundFormatter implements YscopeFieldFormatter { | ||
constructor (options: Nullable<string>) { | ||
if (null !== options) { | ||
throw Error(`RoundFormatter does not support options "${options}"`); | ||
} | ||
} | ||
|
||
// eslint-disable-next-line class-methods-use-this | ||
formatField (field: JsonValue): string { | ||
if ("number" === typeof field) { | ||
field = Math.round(field); | ||
} | ||
|
||
return jsonValueToString(field); | ||
} | ||
} | ||
|
||
export default RoundFormatter; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import {Dayjs} from "dayjs"; | ||
|
||
import {Nullable} from "../../../../typings/common"; | ||
import {YscopeFieldFormatter} from "../../../../typings/formatters"; | ||
import {JsonValue} from "../../../../typings/js"; | ||
import {convertToDayjsTimestamp} from "../../../decoders/JsonlDecoder/utils"; | ||
|
||
|
||
/** | ||
* A formatter for timestamp values, using a specified date-time pattern. | ||
* Options: If no pattern is provided, defaults to ISO 8601 format. | ||
*/ | ||
class TimestampFormatter implements YscopeFieldFormatter { | ||
#dateFormat: Nullable<string> = null; | ||
|
||
constructor (options: Nullable<string>) { | ||
this.#dateFormat = options; | ||
} | ||
|
||
formatField (field: JsonValue): string { | ||
// eslint-disable-next-line no-warning-comments | ||
// TODO: We already parsed the timestamp during deserialization so this is perhaps | ||
// inefficient. However, this field formatter can be used for multiple keys, so using | ||
// the single parsed timestamp by itself would not work. Perhaps in future we can check | ||
// if the key is the same as timestamp key and avoid parsing again. | ||
const timestamp: Dayjs = convertToDayjsTimestamp(field); | ||
if (null === this.#dateFormat) { | ||
return timestamp.format(); | ||
} | ||
|
||
return timestamp.format(this.#dateFormat); | ||
} | ||
} | ||
|
||
export default TimestampFormatter; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import {Nullable} from "../../../typings/common"; | ||
import { | ||
FIELD_PLACEHOLDER_REGEX, | ||
Formatter, | ||
FormatterOptionsType, | ||
REPLACEMENT_CHARACTER, | ||
YscopeFieldFormatter, | ||
YscopeFieldPlaceholder, | ||
} from "../../../typings/formatters"; | ||
import {LogEvent} from "../../../typings/logs"; | ||
import { | ||
getFormattedField, | ||
removeEscapeCharacters, | ||
replaceDoubleBacklash, | ||
splitFieldPlaceholder, | ||
YSCOPE_FIELD_FORMATTER_MAP, | ||
} from "./utils"; | ||
|
||
|
||
/** | ||
* A formatter that uses a YScope format string to format log events into a string. See | ||
* `YscopeFormatterOptionsType` for details about the format string. | ||
*/ | ||
class YscopeFormatter implements Formatter { | ||
readonly #processedFormatString: string; | ||
|
||
#fieldPlaceholders: YscopeFieldPlaceholder[] = []; | ||
|
||
constructor (options: FormatterOptionsType) { | ||
if (options.formatString.includes(REPLACEMENT_CHARACTER)) { | ||
console.warn("Unicode replacement character `U+FFFD` is found in Decoder Format" + | ||
' String, which will appear as "\\".'); | ||
} | ||
|
||
this.#processedFormatString = replaceDoubleBacklash(options.formatString); | ||
this.#parseFieldPlaceholder(); | ||
} | ||
|
||
formatLogEvent (logEvent: LogEvent): string { | ||
const formattedLogFragments: string[] = []; | ||
let lastIndex = 0; | ||
|
||
for (const fieldPlaceholder of this.#fieldPlaceholders) { | ||
const formatStringFragment = | ||
this.#processedFormatString.slice(lastIndex, fieldPlaceholder.range.start); | ||
|
||
formattedLogFragments.push(removeEscapeCharacters(formatStringFragment)); | ||
formattedLogFragments.push(getFormattedField(logEvent, fieldPlaceholder)); | ||
lastIndex = fieldPlaceholder.range.end; | ||
} | ||
|
||
const remainder = this.#processedFormatString.slice(lastIndex); | ||
formattedLogFragments.push(removeEscapeCharacters(remainder)); | ||
|
||
return `${formattedLogFragments.join("")}\n`; | ||
} | ||
|
||
/** | ||
* Parses field placeholders in format string. For each field placeholder, creates a | ||
* corresponding `YscopeFieldFormatter` using the placeholder's field name, formatter type, | ||
* and formatter options. Each `YscopeFieldFormatter` is then stored on the | ||
* class-level array `#fieldPlaceholders`. | ||
* | ||
* @throws Error if `FIELD_PLACEHOLDER_REGEX` does not contain a capture group. | ||
* @throws Error if a formatter type is not supported. | ||
*/ | ||
#parseFieldPlaceholder () { | ||
const placeholderPattern = new RegExp(FIELD_PLACEHOLDER_REGEX, "g"); | ||
const it = this.#processedFormatString.matchAll(placeholderPattern); | ||
for (const match of it) { | ||
// `fullMatch` includes braces and `groupMatch` excludes them. | ||
const [fullMatch, groupMatch]: (string | undefined) [] = match; | ||
|
||
if ("undefined" === typeof groupMatch) { | ||
throw Error("Field placeholder regex is invalid and does not have a capture group"); | ||
} | ||
|
||
const {fieldNameKeys, formatterName, formatterOptions} = | ||
splitFieldPlaceholder(groupMatch); | ||
|
||
let fieldFormatter: Nullable<YscopeFieldFormatter> = null; | ||
if (null !== formatterName) { | ||
const FieldFormatterConstructor = YSCOPE_FIELD_FORMATTER_MAP[formatterName]; | ||
if ("undefined" === typeof FieldFormatterConstructor) { | ||
throw Error(`Formatter ${formatterName} is not currently supported`); | ||
} | ||
fieldFormatter = new FieldFormatterConstructor(formatterOptions); | ||
Comment on lines
+83
to
+87
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. 🛠️ Refactor suggestion Handle unsupported formatter types more gracefully When an unsupported formatter is encountered, the code throws an error: if ("undefined" === typeof FieldFormatterConstructor) {
throw Error(`Formatter ${formatterName} is not currently supported`);
} This could cause the entire application to crash if the format string includes an unknown formatter. Consider handling this scenario more gracefully by notifying the user about the unsupported formatter without throwing an uncaught exception. Possible approaches include:
|
||
} | ||
|
||
this.#fieldPlaceholders.push({ | ||
fieldNameKeys: fieldNameKeys, | ||
fieldFormatter: fieldFormatter, | ||
range: { | ||
start: match.index, | ||
end: match.index + fullMatch.length, | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
|
||
export default YscopeFormatter; |
Uh oh!
There was an error while loading. Please reload this page.