Compare commits

..

6 Commits
0.1.1 ... main

Author SHA1 Message Date
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
2 changed files with 112 additions and 31 deletions

View File

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

View File

@ -1,19 +1,35 @@
import std/[json, sequtils, streams, strutils, terminal, times]
import timeutils
#import docopt
import std/[json, options, sequtils, streams, strutils, terminal, times]
import docopt, timeutils, zero_functional
const VERSION = "0.1.0"
from std/logging import Level
from std/sequtils import toSeq
# const USAGE = """Usage:
# slfmt
#
# Options:
#
# -h, --help Print this usage and help information
const VERSION = "0.2.3"
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
"""
const fieldDisplayOrder = @[
"scope", "level", "ts", "code", "sid", "sub", "msg", "err", "stack", "method", "args"]
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
func decorate(
s: string,
fg = fgDefault,
@ -29,7 +45,7 @@ func decorate(
result &= s & ansiResetCode
proc formatField(name: string, value: JsonNode): string =
proc fullFormatField(name: string, value: JsonNode): string =
result = decorate(name, fgCyan) & ":" & " ".repeat(max(1, 10 - name.len))
var strVal: string = ""
@ -42,38 +58,103 @@ proc formatField(name: string, value: JsonNode): string =
of "err": strVal = decorate(value.getStr, fgRed)
of "msg": strVal = decorate(value.getStr, fgYellow)
of "stack": strVal = decorate(value.getStr, fgBlack, {styleBright})
else:
if value.kind == JString: strVal = decorate(value.getStr)
else: strVal = pretty(value)
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"
else: result &= strVal & "\n"
proc prettyPrintFormat(logLine: string): string =
try:
var logJson = parseJson(logLine)
proc fullFormat(logJson: JsonNode): string =
result = '-'.repeat(terminalWidth()) & "\n"
# Print the known fields in order first
for f in fieldDisplayOrder:
if logJson.hasKey(f):
result &= formatField(f, logJson[f])
result &= fullFormatField(f, logJson[f])
logJson.delete(f)
# 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)
result &= "\n"
except ValueError, JsonParsingError:
result = logLine
proc compactFormat(logJson: JsonNode): string =
let ts = parseIso8601(logJson["ts"].getStr).local.formatIso8601
var level = logJson["level"].getStr
if level == "ERROR": level = decorate(alignLeft(level, 7), fgRed)
elif level == "WARN": level = decorate(alignLeft(level, 7), fgYellow)
else: level = alignLeft(level, 7)
result = "$1 $2 $3: $4" % [
level,
decorate(alignLeft(ts[0..21], 23), fgBlue, {styleBright}),
decorate(logJson["scope"].getStr, fgGreen),
decorate(logJson["msg"].getStr, fgYellow)]
let restNodes = (toSeq(pairs(logJson))) -->
filter(not ["level", "scope", "ts", "msg"].contains(it[0]))
let restMsg = join(restNodes -->
map("$1: $2" % [
decorate(it[0], fgCyan),
if it[1].kind == JString: it[1].getStr
else: pretty(it[1]),
" "]))
if restMsg.len + result.len + 2 < terminalWidth():
result &= " " & restMsg
else:
var line = " "
for (key, val) in restNodes:
let fieldVal =
if val.kind == JString: val.getStr
else: pretty(val)
let field = "$1: $2" % [decorate(key, fgCyan), fieldVal]
if line.len + field.len + 2 > terminalWidth():
result &= "\n " & line
line = " "
line &= field & " "
result &= "\n " & line
proc parseLogLine(logLine: string): JsonNode =
result = parseJson(logLine)
when isMainModule:
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"]
var line: string = ""
let sin = newFileStream(stdin)
while(sin.readLine(line)): stdout.writeLine(prettyPrintFormat(line))
while(sin.readLine(line)):
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))
else: stdout.writeLine(fullFormat(logJson))
except ValueError, JsonParsingError:
stdout.writeLine(line)
except:
stderr.writeLine("slfmt - FATAL: " & getCurrentExceptionMsg())
stderr.writeLine(getCurrentException().getStackTrace())