commit 742d47ba530267eb56d8026a6ed67bf006050ada Author: Jonathan Bernard Date: Fri Jan 3 10:23:00 2025 -0600 Initial implementation. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9095837 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# ignore vim swap files +.*.sw? + +# ignore built binary +/slfmt diff --git a/slfmt.nimble b/slfmt.nimble new file mode 100644 index 0000000..e1a05d5 --- /dev/null +++ b/slfmt.nimble @@ -0,0 +1,14 @@ +# Package + +version = "0.1.0" +author = "Jonathan Bernard" +description = "Small utility to pretty-print strucutured logs." +license = "MIT" +srcDir = "src" +bin = @["slfmt"] + + +# Dependencies + +requires "nim >= 2.2.0" +requires "timeutils" diff --git a/src/slfmt.nim b/src/slfmt.nim new file mode 100644 index 0000000..3f1d80a --- /dev/null +++ b/src/slfmt.nim @@ -0,0 +1,80 @@ +import std/[json, sequtils, streams, strutils, terminal, times] +import timeutils +#import docopt + +const VERSION = "0.1.0" + +# const USAGE = """Usage: +# slfmt +# +# Options: +# +# -h, --help Print this usage and help information + +const fieldDisplayOrder = @[ + "scope", "level", "ts", "code", "sid", "sub", "msg", "err", "stack", "method", "args"] + +func decorate( + s: string, + fg = fgDefault, + style: set[Style] = {}): string = + + result = "" + + 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 = + result = decorate(name, fgCyan) & ":" & " ".repeat(max(1, 10 - name.len)) + + var strVal: string = "" + case name: + of "ts": + let dt = parseIso8601(value.getStr) + strVal = decorate(dt.local.formatIso8601 & " (local) ", fgBlue, {styleBright}) & + dt.utc.formatIso8601 & " (UTC)" + of "sid", "sub": strVal = decorate(value.getStr, fgGreen) + of "err": strVal = decorate(value.getStr, fgRed) + of "msg": strVal = decorate(value.getStr, fgYellow) + of "stack": strVal = decorate(value.getStr, fgBlack, {styleBright}) + else: strVal = pretty(value) + + let valLines = splitLines(strVal) + if name.len > 10 or strVal.len + 16 > 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) + + result = '-'.repeat(terminalWidth()) + + # Print the known fields in order first + for f in fieldDisplayOrder: + if logJson.hasKey(f): + result &= formatField(f, logJson[f]) + logJson.delete(f) + + # Print the rest of the fields + for (key, val) in pairs(logJson): result &= formatField(key, val) + + result &= "\n" + + except ValueError, JsonParsingError: + result = logLine + +when isMainModule: + try: + var line: string = "" + let sin = newFileStream(stdin) + while(sin.readLine(line)): stdout.writeLine(prettyPrintFormat(line)) + except: + stderr.writeLine("slfmt - FATAL: " & getCurrentExceptionMsg()) + stderr.writeLine(getCurrentException().getStackTrace()) + quit(QuitFailure)