11 Commits
0.1.0 ... 1.0.0

Author SHA1 Message Date
9219d3e86e Don't show sample highlighting for expected fields (which will never be highlighted). 2025-12-19 09:20:50 -06:00
29bca76cf1 Remove multi-threaded, interactive input functionality.
It didn't really work when reading from /dev/tty anyways, as the program
upstream from slfmt is probably also reading from /dev/tty and has
priority.
2025-12-19 09:14:47 -06:00
c269020227 Add expectations. 2025-12-19 09:10:13 -06:00
45db33bf9e Use system-native linebreaks. 2025-07-14 10:33:13 -05:00
8b0a684dab Explicitly require docopt v0.7.1 or above. 2025-05-13 09:56:44 -05:00
4efc72e8ae Correctly handle nested JSON fields when using compact formatting. 2025-02-04 17:35:00 -06:00
2291b75c0a Tweak line wrapping for long-keyed, short-values fields. 2025-01-20 23:06:07 -06:00
11cc9c9a39 Fix bug when a line happens to parse as JSON but isn't an object.
For example, if `false` happened to be the only value on a line, this
would be parsed as valid JSON (a boolean) but it is not a structured log
message that slfmt should try to parse and format.
2025-01-20 21:24:22 -06:00
6c978f32cc Add --compact option for simpler output. 2025-01-19 18:40:38 -06:00
34add1a729 Add the ability to filter by log level and namespace. 2025-01-04 23:01:17 -06:00
982680d972 Add README, fix headline printing. 2025-01-03 10:54:54 -06:00
3 changed files with 523 additions and 52 deletions

87
README.md Normal file
View File

@@ -0,0 +1,87 @@
# Structured Log Formatter
**Terminal utility to pretty-print JSON-strucutred log lines.**
## Usage
`slfmt` is intended to be used to filter logging output from a process. It
reads from `stdin` and writes to `stdout`. It expects to see a JSON object on
each line of input, and will pretty-print the object to `stdout`. Escape codes
in the log lines (`\n` for newlines, etc.) are supported and will be expanded
in the output.
Any lines that are not valid JSON objects will be printed as-is to `stdout`.
Example:
```bash
./run-server | slfmt
```
Sample Output:
```
---------------------------------------------------------------------------------------
scope: "server/rest_api"
level: "ERROR"
ts:
2025-01-03T08:46:35-06:00 (local) 2025-01-03T14:46:35Z (UTC)
msg: unhandled exception (DbError)
err:
ERROR: relation "users" does not exist
LINE 1: SELECT id FROM users WHERE id = '43422d65-6874-44ce-b9b3-f8c...
^
stack:
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy.nim(519) workerProc
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy.nim(450) runTask
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy/routers.nim(271) :anonymous
/home/username/projects/server/api/src/main/nim/server_api/api.nim(65) :anonymous
/home/username/projects/server/api/src/main/nim/server_api/auth.nim(74) extractSession
/home/username/projects/fiber-orm-nim/src/fiber_orm.nim(599) createOrUpdateUser
/home/username/projects/fiber-orm-nim/src/fiber_orm.nim(384) createOrUpdateRecord
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/db_connector-0.1.0-d68319e3785fa937f0465ea915e942b61b6b5442/db_connector/db_postgres.nim(497) getAllRows
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/db_connector-0.1.0-d68319e3785fa937f0465ea915e942b61b6b5442/db_connector/db_postgres.nim(174) setupQuery
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/db_connector-0.1.0-d68319e3785fa937f0465ea915e942b61b6b5442/db_connector/db_postgres.nim(116) dbError
[[reraised from:
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy.nim(519) workerProc
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy.nim(450) runTask
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy/routers.nim(271) :anonymous
/home/username/projects/server/api/src/main/nim/server_api/api.nim(65) :anonymous
/home/username/projects/server/api/src/main/nim/server_api/auth.nim(74) extractSession
/home/username/projects/fiber-orm-nim/src/fiber_orm.nim(727) createOrUpdateUser
]]
---------------------------------------------------------------------------------------
scope: "server/rest_api"
level: "ERROR"
ts:
2025-01-03T08:46:35-06:00 (local) 2025-01-03T14:46:35Z (UTC)
msg: unhandled exception (DbError)
err:
ERROR: relation "users" does not exist
LINE 1: SELECT id FROM users WHERE id = '43422d65-6874-44ce-b9b3-f8c...
^
stack:
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy.nim(519) workerProc
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy.nim(450) runTask
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy/routers.nim(271) :anonymous
/home/username/projects/server/api/src/main/nim/server_api/api.nim(65) :anonymous
/home/username/projects/server/api/src/main/nim/server_api/auth.nim(74) extractSession
/home/username/projects/fiber-orm-nim/src/fiber_orm.nim(599) createOrUpdateUser
/home/username/projects/fiber-orm-nim/src/fiber_orm.nim(384) createOrUpdateRecord
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/db_connector-0.1.0-d68319e3785fa937f0465ea915e942b61b6b5442/db_connector/db_postgres.nim(497) getAllRows
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/db_connector-0.1.0-d68319e3785fa937f0465ea915e942b61b6b5442/db_connector/db_postgres.nim(174) setupQuery
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/db_connector-0.1.0-d68319e3785fa937f0465ea915e942b61b6b5442/db_connector/db_postgres.nim(116) dbError
[[reraised from:
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy.nim(519) workerProc
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy.nim(450) runTask
/home/username/.local/share/mise/installs/nim/2.2.0/nimble/pkgs2/mummy-0.4.5-cb7f70cd4d6fd3a563e00e664cfbd8cbd3c39b79/mummy/routers.nim(271) :anonymous
/home/username/projects/server/api/src/main/nim/server_api/api.nim(65) :anonymous
/home/username/projects/server/api/src/main/nim/server_api/auth.nim(74) extractSession
/home/username/projects/fiber-orm-nim/src/fiber_orm.nim(727) createOrUpdateUser
]]
```

View File

@@ -1,6 +1,6 @@
# Package # Package
version = "0.1.0" version = "1.0.0"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Small utility to pretty-print strucutured logs." description = "Small utility to pretty-print strucutured logs."
license = "MIT" license = "MIT"
@@ -10,5 +10,5 @@ bin = @["slfmt"]
# Dependencies # Dependencies
requires "nim >= 2.2.0" requires @["nim >= 2.2.0", "docopt >= 0.7.1"]
requires "timeutils" requires @["cliutils >= 0.11.0", "timeutils", "zero_functional"]

View File

@@ -1,80 +1,464 @@
import std/[json, sequtils, streams, strutils, terminal, times] import std/[atomics, json, options, os, posix, sequtils, setutils, strutils,
import timeutils terminal, times, unicode]
#import docopt import cliutils, docopt, timeutils, zero_functional
const VERSION = "0.1.0" from std/logging import Level
import std/nre except toSeq
const VERSION = "1.0.0"
const USAGE = """Usage:
slfmt [options]
Options:
-h, --help Print this usage and help information
-c, --compact Compact output
-l, --log-level <lvl> Only show log events at or above this level
-n, --namespace <ns> Only show log events from this namespace
-e, --expected <expected>
Add a filter for something expected to be present in the logs. It's
absence will be flagged. On the command-line, <expected> should be
formatted as:
field-name:regex-pattern:label
<label> is optional and when provided is used as the value displayed
when this expectation is flagged. If the log stream is not a structured
log, the <field-name> is ignored and the log text is matched directly
against the log line. Multiple expectations can be provided, separating
each with ';'.
-E, --unexpected <unexpected>
Add a filter for something that should not be present in the logs. It's
presence will be flagged. On the command-line, <unexpected> should be
formatted as:
field-name:regex-pattern:label
This option can be provided multiple times. <label> is optional and
when provided is used as the value displayed when this expectation is
flagged. If the log stream is not a structured log, the <field-name> is
ignored and the log text is matched directly against the log line.
Multiple expectations can be provided, separating each with ';'.
--config <cfgFile>
Read expectation data from a json config file formatted as follows:
[
{
"fieldName": "<field-name>",
"label": "<label value>", // optional
"pattern": "<regex pattern>",
"expected": false,
"style": { // optional
"fg": "brightwhite",
"bg": "red",
"style": ["bold"]
},
// ...
]
The file should be a valid JSON array of expectation objects. Each
object must include fieldName, label, pattern, and expected fields. The
expected field determines whether this field should or should not be
found in the output. If set to true, this expectation operates like
--expected. If set to false it operates like --unexpected.
If label is not present or is null, the fieldName:pattern combination
will be used, similar to the CLI arguments behavior.
If the style field is set, it must be an object with fg, bg, and style
keys. There are three types of values that can be passed to fg and bg:
1. A string value representing named terminal color. The list of
recognized colors is
black, red, green, yellow, blue, magenta, cyan, white, default
There are also bright versions of each (except default) for terminals
that support them. For example, brightred, brightblack, etc.
For example:
"fg": "red",
2. A number representing an 8-bit color value (1-255). For example:
"bg": 60,
3. An object with r, g, and b fields representing a 24-bit Truecolor
value. For example:
"fg": { "r": 22, "g": 57, "b": 105 }
If there is no style set, one of the default styles is used (like when
using the CLI args to define expectations).
"""
type
CtrlCmd = enum ccQuit, ccReset
Expectation = ref object
fieldName: string
label: string
pattern: Regex
expected: bool
count: int
termStyleCode: string # ANSI escape code
# const USAGE = """Usage:
# slfmt
#
# Options:
#
# -h, --help Print this usage and help information
const fieldDisplayOrder = @[ const fieldDisplayOrder = @[
"scope", "level", "ts", "code", "sid", "sub", "msg", "err", "stack", "method", "args"] "scope", "level", "ts", "code", "sid", "sub", "msg", "err", "stack", "method", "args"]
func decorate(
s: string,
fg = fgDefault,
style: set[Style] = {}): string =
result = "" let FORMATTING_REGEX* = re("\x1b\\[([0-9;]*)([a-zA-Z])")
if style != {}:
result &= toSeq(items(style)).mapIt(ansiStyleCode(it)).join("")
if fg != fgDefault: result &= ansiForegroundColorCode(fg)
result &= s & ansiResetCode
proc formatField(name: string, value: JsonNode): string = const EXPECTATION_STYLES = [
result = decorate(name, fgCyan) & ":" & " ".repeat(max(1, 10 - name.len)) (ansiEscSeq(fg = cBrightWhite) & ansiEscSeq(bg = (r: 105, g: 22, b:33))), # red
(ansiEscSeq(fg = cBrightWhite) & ansiEscSeq(bg = (r: 62, g: 100, b:15))), # green
(ansiEscSeq(fg = cBrightWhite) & ansiEscSeq(bg = (r: 22, g: 57, b:105))), # blue
(ansiEscSeq(fg = cBrightWhite) & ansiEscSeq(bg = (r: 79, g: 79, b:32))), # yellow
(ansiEscSeq(fg = cBrightWhite) & ansiEscSeq(bg = (r: 73, g: 55, b:78))), # magenta
(ansiEscSeq(fg = cBrightWhite) & ansiEscSeq(bg = (r: 41, g: 84, b:98))) # cyan
]
var stopFlag: Atomic[bool]
proc handleSignal(sig: cint) {.noconv.} = stopFlag.store(true)
func getOrFail(n: JsonNode, key: string): JsonNode =
if not n.contains(key):
raise newException(ValueError, "missing " & key &
" field in the following JSON node:\n" & n.pretty)
n[key]
func parseLogLevel(s: string): Level =
case s.toUpper
of "DEBUG": result = Level.lvlDebug
of "INFO": result = Level.lvlInfo
of "NOTICE": result = Level.lvlNotice
of "WARN": result = Level.lvlWarn
of "ERROR": result = Level.lvlError
of "FATAL": result = Level.lvlFatal
else: result = Level.lvlAll
proc processFormattedFieldExpectations(
expectations: seq[Expectation] = @[],
value: string): string =
result = value
for exp in expectations:
let match = find(stripAnsi(value), exp.pattern)
if match.isSome:
inc exp.count
if not exp.expected:
let bounds = match.get.matchBounds
result =
ansiAwareSubstring(result, (0..<bounds.a)) &
exp.termStyleCode &
stripAnsi(ansiAwareSubstring(result, (bounds.a .. bounds.b))) &
RESET_FORMATTING &
ansiAwareSubstring(result, (bounds.b + 1 ..< ^1))
proc processFormattedFieldExpectations(
name: string,
expectations: seq[Expectation],
value: string): string =
processFormattedFieldExpectations(
expectations.filterIt(it.fieldName == name),
value)
proc fullFormatField(
name: string,
value: JsonNode,
expectations: seq[Expectation] = @[]): string =
result = color(name, cCyan) & ":" & " ".repeat(max(1, 10 - name.len))
var strVal: string = "" var strVal: string = ""
case name: case name:
of "ts": of "ts":
let dt = parseIso8601(value.getStr) let dt = parseIso8601(value.getStr)
strVal = decorate(dt.local.formatIso8601 & " (local) ", fgBlue, {styleBright}) & strVal =
color(dt.local.formatIso8601 & " (local) ", cBrightBlue) &
dt.utc.formatIso8601 & " (UTC)" dt.utc.formatIso8601 & " (UTC)"
of "sid", "sub": strVal = decorate(value.getStr, fgGreen) of "sid", "sub": strVal = color(value.getStr, cGreen)
of "err": strVal = decorate(value.getStr, fgRed) of "err": strVal = color(value.getStr, cRed)
of "msg": strVal = decorate(value.getStr, fgYellow) of "msg": strVal = color(value.getStr, cYellow)
of "stack": strVal = decorate(value.getStr, fgBlack, {styleBright}) of "stack": strVal = color(value.getStr, cBrightBlack)
else:
if value.kind == JString: strVal = value.getStr
else: strVal = pretty(value) else: strVal = pretty(value)
strVal = processFormattedFieldExpectations(name, expectations, strVal)
let valLines = splitLines(strVal) let valLines = splitLines(strVal)
if name.len > 10 or strVal.len + 16 > terminalWidth() or valLines.len > 1: if name.len + strVal.len + 6 > terminalWidth() or valLines.len > 1:
result &= "\n" & valLines.mapIt(" " & it).join("\n") & "\n" result &= "\p" & valLines.mapIt(" " & it).join("\p") & "\p"
else: result &= strVal & "\n" else: result &= strVal & "\p"
proc prettyPrintFormat(logLine: string): string = proc fullFormat(logJson: JsonNode, expectations: seq[Expectation] = @[]): string =
try: result = '-'.repeat(terminalWidth()) & "\p"
var logJson = parseJson(logLine)
result = '-'.repeat(terminalWidth())
# Print the known fields in order first # Print the known fields in order first
for f in fieldDisplayOrder: for f in fieldDisplayOrder:
if logJson.hasKey(f): if logJson.hasKey(f):
result &= formatField(f, logJson[f]) result &= fullFormatField(f, logJson[f], expectations)
logJson.delete(f) logJson.delete(f)
# Print the rest of the fields # Print the rest of the fields
for (key, val) in pairs(logJson): result &= formatField(key, val) for (key, val) in pairs(logJson):
result &= fullFormatField(key, val, expectations)
result &= "\n" result &= "\p"
proc compactFormat(logJson: JsonNode, expectations: seq[Expectation] = @[]): string =
var ts = parseIso8601(logJson["ts"].getStr).local.formatIso8601
ts = color(alignLeft(ts[0..21], 23), cBrightBlue)
ts = processFormattedFieldExpectations("ts", expectations, ts)
var level = logJson["level"].getStr
if level == "ERROR": level = color(alignLeft(level, 7), cRed)
elif level == "WARN": level = color(alignLeft(level, 7), cYellow)
else: level = alignLeft(level, 7)
level = processFormattedFieldExpectations("level", expectations, level)
let scope = processFormattedFieldExpectations("scope", expectations,
color(logJson["scope"].getStr, cGreen))
let msg = processFormattedFieldExpectations("msg", expectations,
color(logJson["msg"].getStr, cYellow))
result = "$1 $2 $3: $4" % [ level, ts, scope, msg]
let restNodes = (toSeq(pairs(logJson))) -->
filter(not ["level", "scope", "ts", "msg"].contains(it[0]))
let restMsg = join(restNodes -->
map("$1: $2" % [
color(it[0], cCyan),
processFormattedFieldExpectations(it[0], expectations,
if it[1].kind == JString: it[1].getStr
else: pretty(it[1])),
" "]))
if runeLen(stripAnsi(restMsg)) + runeLen(stripAnsi(result)) + 2 < terminalWidth():
result &= " " & restMsg
else:
var line = " "
for (key, val) in restNodes:
let fieldVal = processFormattedFieldExpectations(key, expectations,
if val.kind == JString: val.getStr
else: pretty(val))
let field = "$1: $2" % [color(key, cCyan), fieldVal]
if runeLen(stripAnsi(line)) + runeLen(stripAnsi(field)) + 2 > terminalWidth():
result &= "\p " & line
line = " "
line &= field & " "
result &= "\p " & line
func formatExpectationLabels(expectations: seq[Expectation]): string =
let maxLabelLen = expectations.mapIt(runeLen(it.label)).max(system.cmp)
let maxCountLen = runeLen($expectations.mapIt(it.count).max(system.cmp))
let expectationLabels = expectations.map(proc (exp: Expectation): string =
let label = alignLeft(exp.label, maxLabelLen)
let count = alignLeft($exp.count, maxCountLen)
if exp.expected:
result = label & " - "
else:
result = exp.termStyleCode & label & RESET_FORMATTING & " - "
if (exp.expected and exp.count == 0) or
(not exp.expected and exp.count > 0):
result &= termFmt(count, fg = cBrightRed, bg = cDefault, style = {tsBold})
else:
result &= count
)
return "[ " & expectationLabels.join(" | ") & " ]"
proc parseLogLine(logLine: string): JsonNode =
result = parseJson(logLine)
proc parseExpectations(args: Table[string, docopt.Value]): seq[Expectation] =
result = @[]
for isExpected in [false, true]:
let argName = if isExpected: "--expected" else: "--unexpected"
if args[argName]:
let exptArgs = split($args[argName], ';')
for ea in exptArgs:
let parts = split(ea, ':', 2)
result.add(Expectation(
fieldName: parts[0],
pattern: re(parts[1]),
termStyleCode: EXPECTATION_STYLES[result.len mod EXPECTATION_STYLES.len],
label:
if parts.len > 2: parts[2]
else: ea,
expected: isExpected,
count: 0))
if args["--config"]:
let filename = $args["--config"]
if not fileExists(filename):
stderr.writeLine("slfmt - WARN: " &
filename & " does not exist, ignoring")
try:
let cfg: JsonNode = parseFile(filename)
for expJson in cfg.getElems:
var exp = Expectation(
fieldName: expJson.getOrFail("fieldName").getStr,
pattern: re(expJson.getOrFail("pattern").getStr),
expected: expJson.getOrFail("expected").getBool,
termStyleCode: EXPECTATION_STYLES[result.len mod EXPECTATION_STYLES.len],
label:
if expJson.contains("label") and expJson["label"].kind == JString:
expJson["label"].getStr
else:
expJson.getOrFail("fieldName").getStr & ":" &
expJson.getOrFail("pattern").getStr)
if expJson.contains("style"):
let styleJson = expJson["style"]
let fmtStyles: set[TerminalStyle] = styleJson
.getOrFail("style")
.getElems
.mapIt(parseEnum[TerminalStyle]("ts" & it.getStr))
.toSet
let fgJson = styleJson.getOrFail("fg")
let bgJson = styleJson.getOrFail("bg")
if fgJson.kind == JString and bgJson.kind == JString:
# simple case, both fg and bg are TerminalColors
exp.termStyleCode = ansiEscSeq(
fg = parseEnum[TerminalColors]("c" & fgJson.getStr),
bg = parseEnum[TerminalColors]("c" & bgJson.getStr),
fmtStyles)
else:
exp.termStyleCode = ansiEscSeq(fmtStyles)
case fgJson.kind
of JString:
exp.termStyleCode &= ansiEscSeq(
fg = parseEnum[TerminalColors]("c" & fgJson.getStr))
of JInt: exp.termStyleCode &= ansiEscSeq(fg = fgJson.getInt)
of JObject:
exp.termStyleCode &= ansiEscSeq(fg = (
r: fgJson.getOrFail("r").getInt,
g: fgJson.getOrFail("g").getInt,
b: fgJson.getOrFail("b").getInt))
else:
raise newException(ValueError,
"The 'fg' field must be a string, number, or object.")
case bgJson.kind
of JString:
exp.termStyleCode &= ansiEscSeq(
bg = parseEnum[TerminalColors]("c" & bgJson.getStr))
of JInt: exp.termStyleCode &= ansiEscSeq(bg = bgJson.getInt)
of JObject:
exp.termStyleCode &= ansiEscSeq(bg = (
r: bgJson.getOrFail("r").getInt,
g: bgJson.getOrFail("g").getInt,
b: bgJson.getOrFail("b").getInt))
else:
raise newException(ValueError,
"The 'bg' field must be a string, number, or object.")
result.add(exp)
except:
stderr.writeLine("slfmt - WARN: unable to parse config file, ignoring.")
stderr.writeLine("slfmt - DEBUG: " & getCurrentExceptionMsg())
proc eraseAndWriteLine(f: File, s: string) =
f.writeline(eraseLine(emAll) & s)
except ValueError, JsonParsingError:
result = logLine
when isMainModule: when isMainModule:
try: try:
let args = docopt(USAGE, version = VERSION)
let logLevel =
if args["--log-level"]: some(parseLogLevel($args["--log-level"]))
else: none[Level]()
let namespace =
if args["--namespace"]: some($args["--namespace"])
else: none[string]()
let compact = args["--compact"]
let expectations = parseExpectations(args)
signal(SIGINT, handleSignal)
signal(SIGTERM, handleSignal)
stdout.hideCursor
stdout.writeLine("") # burn a line
var line: string = "" var line: string = ""
let sin = newFileStream(stdin) while stdin.readLine(line) and not stopFlag.load():
while(sin.readLine(line)): stdout.writeLine(prettyPrintFormat(line)) if expectations.len > 0:
stdout.eraseLine
stdout.cursorUp
stdout.eraseLine
try:
let logJson = parseLogLine(line)
if logJson.kind != JObject:
raise newException(JsonParsingError, "Expected a JSON object")
if logLevel.isSome and logJson.hasKey("level"):
let lvl = parseLogLevel(logJson["level"].getStr)
if lvl < logLevel.get: continue
if namespace.isSome and logJson.hasKey("scope"):
if not logJson["scope"].getStr.startsWith(namespace.get): continue
if compact: stdout.writeLine(compactFormat(logJson, expectations))
else: stdout.writeLine(fullFormat(logJson, expectations))
except ValueError, JsonParsingError:
stdout.writeLine(processFormattedFieldExpectations(expectations, line))
finally:
if expectations.len > 0:
let labelsMsg = formatExpectationLabels(expectations)
stdout.writeLine(termFmt(repeat("", terminalWidth()),
fg = cDefault, bg = cDefault, style = {tsDim}))
stdout.write(labelsMsg)
stdout.cursorBackward(len(stripAnsi(labelsMsg)))
stdout.flushFile
except: except:
stderr.writeLine("slfmt - FATAL: " & getCurrentExceptionMsg()) stderr.writeLine("slfmt - FATAL: " & getCurrentExceptionMsg())
stderr.writeLine(getCurrentException().getStackTrace()) stderr.writeLine(getCurrentException().getStackTrace())
quit(QuitFailure) quit(QuitFailure)
finally:
stdout.writeLine(RESET_FORMATTING)
stdout.write(eraseLine(emAll))
stdout.showCursor