2 Commits
0.1.0 ... 0.1.2

3 changed files with 152 additions and 28 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 = "0.1.2"
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"]
requires "timeutils" requires "timeutils"

View File

@ -1,19 +1,34 @@
import std/[json, sequtils, streams, strutils, terminal, times] import std/[json, options, sequtils, streams, strutils, terminal, times]
import timeutils import timeutils
#import docopt import docopt
const VERSION = "0.1.0" from std/logging import Level
# const USAGE = """Usage: const VERSION = "0.1.2"
# slfmt
# const USAGE = """Usage:
# Options: slfmt [options]
#
# -h, --help Print this usage and help information Options:
-h, --help Print this usage and help information
-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 = @[ 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 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( func decorate(
s: string, s: string,
fg = fgDefault, fg = fgDefault,
@ -42,6 +57,8 @@ proc formatField(name: string, value: JsonNode): string =
of "err": strVal = decorate(value.getStr, fgRed) of "err": strVal = decorate(value.getStr, fgRed)
of "msg": strVal = decorate(value.getStr, fgYellow) of "msg": strVal = decorate(value.getStr, fgYellow)
of "stack": strVal = decorate(value.getStr, fgBlack, {styleBright}) of "stack": strVal = decorate(value.getStr, fgBlack, {styleBright})
else:
if value.kind == JString: strVal = decorate(value.getStr)
else: strVal = pretty(value) else: strVal = pretty(value)
let valLines = splitLines(strVal) let valLines = splitLines(strVal)
@ -49,11 +66,8 @@ proc formatField(name: string, value: JsonNode): string =
result &= "\n" & valLines.mapIt(" " & it).join("\n") & "\n" result &= "\n" & valLines.mapIt(" " & it).join("\n") & "\n"
else: result &= strVal & "\n" else: result &= strVal & "\n"
proc prettyPrintFormat(logLine: string): string = proc prettyPrintFormat(logJson: JsonNode): string =
try: result = '-'.repeat(terminalWidth()) & "\n"
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:
@ -66,14 +80,37 @@ proc prettyPrintFormat(logLine: string): string =
result &= "\n" result &= "\n"
except ValueError, JsonParsingError: proc parseLogLine(logLine: string): JsonNode =
result = logLine result = parseJson(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]()
var line: string = "" var line: string = ""
let sin = newFileStream(stdin) let sin = newFileStream(stdin)
while(sin.readLine(line)): stdout.writeLine(prettyPrintFormat(line)) while(sin.readLine(line)):
try:
let logJson = parseLogLine(line)
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
stdout.writeLine(prettyPrintFormat(logJson))
except ValueError, JsonParsingError:
stdout.writeLine(line)
except: except:
stderr.writeLine("slfmt - FATAL: " & getCurrentExceptionMsg()) stderr.writeLine("slfmt - FATAL: " & getCurrentExceptionMsg())
stderr.writeLine(getCurrentException().getStackTrace()) stderr.writeLine(getCurrentException().getStackTrace())