Compare commits
	
		
			8 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 45db33bf9e | |||
| 8b0a684dab | |||
| 4efc72e8ae | |||
| 2291b75c0a | |||
| 11cc9c9a39 | |||
| 6c978f32cc | |||
| 34add1a729 | |||
| 982680d972 | 
							
								
								
									
										87
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								README.md
									
									
									
									
									
										Normal 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
 | 
			
		||||
  ]]
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
# Package
 | 
			
		||||
 | 
			
		||||
version       = "0.1.0"
 | 
			
		||||
version       = "0.2.4"
 | 
			
		||||
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"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										131
									
								
								src/slfmt.nim
									
									
									
									
									
								
							
							
						
						
									
										131
									
								
								src/slfmt.nim
									
									
									
									
									
								
							@@ -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.4"
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
    result &= "\n" & valLines.mapIt("  " & it).join("\n") & "\n"
 | 
			
		||||
  else: result &= strVal & "\n"
 | 
			
		||||
  if name.len + strVal.len + 6 > terminalWidth() or valLines.len > 1:
 | 
			
		||||
    result &= "\p" & valLines.mapIt("  " & it).join("\p") & "\p"
 | 
			
		||||
  else: result &= strVal & "\p"
 | 
			
		||||
 | 
			
		||||
proc prettyPrintFormat(logLine: string): string =
 | 
			
		||||
  try:
 | 
			
		||||
    var logJson = parseJson(logLine)
 | 
			
		||||
 | 
			
		||||
    result = '-'.repeat(terminalWidth())
 | 
			
		||||
proc fullFormat(logJson: JsonNode): string =
 | 
			
		||||
  result = '-'.repeat(terminalWidth()) & "\p"
 | 
			
		||||
 | 
			
		||||
  # 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"
 | 
			
		||||
  result &= "\p"
 | 
			
		||||
 | 
			
		||||
  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 &= "\p  " & line
 | 
			
		||||
        line = "  "
 | 
			
		||||
      line &= field & "  "
 | 
			
		||||
    result &= "\p  " & 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())
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user