-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(formatter): Add YScope formatter for structured logs and remove …
…Logback-style formatter. (#123) Co-authored-by: Junhao Liao <[email protected]>
- Loading branch information
1 parent
7e6073f
commit d2ebacf
Showing
11 changed files
with
492 additions
and
183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
29 changes: 29 additions & 0 deletions
29
src/services/formatters/YscopeFormatter/FieldFormatters/RoundFormatter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
35 changes: 35 additions & 0 deletions
35
src/services/formatters/YscopeFormatter/FieldFormatters/TimestampFormatter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
this.#fieldPlaceholders.push({ | ||
fieldNameKeys: fieldNameKeys, | ||
fieldFormatter: fieldFormatter, | ||
range: { | ||
start: match.index, | ||
end: match.index + fullMatch.length, | ||
}, | ||
}); | ||
} | ||
} | ||
} | ||
|
||
export default YscopeFormatter; |
Oops, something went wrong.