Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Define the Chronicle output styles as nim-serialization formats #82

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions chronicles.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ license = "Apache License 2.0"
skipDirs = @["tests"]

requires "nim >= 1.2.0"
requires "serialization"
requires "json_serialization"
requires "testutils < 2.0.0"

Expand Down
233 changes: 49 additions & 184 deletions chronicles/log_output.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import
strutils, times, macros, options, os,
dynamic_scope_types
dynamic_scope_types,
serialization,
textlineserializer,
textblockserializer

when defined(js):
import
Expand All @@ -21,9 +24,6 @@ else:

type OutStr = string

const
propColor = fgBlue
topicsColor = fgYellow

export
LogLevel
Expand Down Expand Up @@ -56,16 +56,14 @@ type
AnyFileOutput = FileOutput|StdOutOutput|StdErrOutput
AnyOutput = AnyFileOutput|SysLogOutput|BufferedOutput|PassThroughOutput

TextLineRecord*[Output;
timestamps: static[TimestampsScheme],
colors: static[ColorScheme]] = object
output*: Output
level: LogLevel

TextBlockRecord*[Output;
timestamps: static[TimestampsScheme],
colors: static[ColorScheme]] = object
LogRecord*[Output;
logformat: static[LogFormat],
Copy link
Contributor

@zah zah May 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry my explanation wasn't clear enough. The names of the formats such as Json, TextBlock and TextLine are Nim type names. You don't need to (and you shouldn't) pass them as static parameters.

The correct definition is

type
  LogRecord[Output; Format; timestamps: static[TimestampsScheme], ...] = object
    output*: Output 
    writer*: WriterType(Format)

You can initialize the writer by passing a stream to its init function like it's done here:
https://github.com/status-im/nim-json-serialization/blob/master/json_serialization/writer.nim#L20-L26

I haven't tried the above and perhaps the use of WriterType is pushing Nim a bit too far. If that's the case, you can make the LogRecord parametric on a Writer type directly (instead of Format).

timestamps: static[TimestampsScheme],
colors: static[ColorScheme]] = object
output*: Output
internalStream: OutputStream
tlwriter: TextLineWriter[timestamps, colors]
tbwriter: TextBlockWriter[timestamps, colors]

StreamOutputRef*[Stream; outputId: static[int]] = object

Expand Down Expand Up @@ -255,8 +253,8 @@ proc selectRecordType(s: var StreamCodeNodes, sink: SinkSpec): NimNode =
# Determine the head symbol of the instantiation
let RecordType = case sink.format
of json: bnd"JsonRecord"
of textLines: bnd"TextLineRecord"
of textBlocks: bnd"TextBlockRecord"
of textLines: bnd"LogRecord"
of textBlocks: bnd"LogRecord"

result = newTree(nnkBracketExpr, RecordType)

Expand All @@ -280,6 +278,9 @@ proc selectRecordType(s: var StreamCodeNodes, sink: SinkSpec): NimNode =
else:
result.add selectOutputType(s, sink.destinations[0])

if sink.format != json:
result.add newIdentNode($sink.format)

result.add newIdentNode($sink.timestamps)

# Set the color scheme for the record types that require it
Expand Down Expand Up @@ -455,29 +456,6 @@ template applyStyle(record, style) =
elif record.colors == NativeColors:
setStyle(getOutputStream(record.output), {style})

template levelToStyle(lvl: LogLevel): untyped =
case lvl
of TRACE: (fgGreen, true)
of DEBUG: (fgGreen, true)
of INFO: (fgGreen, false)
of NOTICE:(fgYellow, false)
of WARN: (fgYellow, true)
of ERROR: (fgRed, false)
of FATAL: (fgRed, true)
of NONE: (fgWhite, false)

template shortName(lvl: LogLevel): string =
# Same-length strings make for nice alignment
case lvl
of TRACE: "TRC"
of DEBUG: "DBG"
of INFO: "INF"
of NOTICE:"NOT"
of WARN: "WRN"
of ERROR: "ERR"
of FATAL: "FAT"
of NONE: " "

template appendLogLevelMarker(r: var auto, lvl: LogLevel, align: bool) =
when r.colors != NoColors:
let (color, bright) = levelToStyle(lvl)
Expand All @@ -487,152 +465,6 @@ template appendLogLevelMarker(r: var auto, lvl: LogLevel, align: bool) =
else: $lvl)
resetColors(r)

template appendHeader(r: var TextLineRecord | var TextBlockRecord,
lvl: LogLevel,
topics: string,
name: string,
pad: bool) =
# Log level comes first - allows for easy regex match with ^
appendLogLevelMarker(r, lvl, true)

when r.timestamps != NoTimestamps:
append(r.output, " ")
writeTs(r)

if name.len > 0:
# no good way to tell how much padding is going to be needed so we
# choose an arbitrary number and use that - should be fine even for
# 80-char terminals
# XXX: This should be const, but the compiler fails with an ICE
let padding = repeat(' ', if pad: 42 - min(42, name.len) else: 0)

append(r.output, " ")
applyStyle(r, styleBright)
append(r.output, name)
append(r.output, padding)
resetColors(r)

if topics.len > 0:
append(r.output, " topics=\"")
fgColor(r, topicsColor, true)
append(r.output, topics)
resetColors(r)
append(r.output, "\"")

#
# A LogRecord is a single "logical line" in the output.
#
# 1. It's instantiated by the log statement.
#
# 2. It's initialized with a call to `initLogRecord`.
#
# 3. Zero or more calls to `setFirstProperty` and `setPropery` are
# executed with the current lixical and dynamic bindings.
#
# 4. Finally, `flushRecord` should wrap-up the record and flush the output.
#

#
# Text line records:
#

proc initLogRecord*(r: var TextLineRecord,
lvl: LogLevel,
topics: string,
name: string) =
r.level = lvl
appendHeader(r, lvl, topics, name, true)

proc setProperty*(r: var TextLineRecord, key: string, val: auto) =
append(r.output, " ")
let valText = $val

var
escaped: string
valueToWrite: ptr string

# Escaping is done to avoid issues with quoting and newlines
# Quoting is done to distinguish strings with spaces in them from a new
# key-value pair
# This is similar to how it's done in logfmt:
# https://github.com/csquared/node-logfmt/blob/master/lib/stringify.js#L13
let
needsEscape = valText.find(NewLines + {'"', '\\'}) > -1
needsQuote = valText.find({' ', '='}) > -1

if needsEscape or needsQuote:
escaped = newStringOfCap(valText.len + valText.len div 8)
if needsEscape:
# addQuoted adds quotes and escapes a bunch of characters
# XXX addQuoted escapes more characters than what we look for in above
# needsEscape check - it's a bit weird that way
addQuoted(escaped, valText)
elif needsQuote:
add(escaped, '"')
add(escaped, valText)
add(escaped, '"')
valueToWrite = addr escaped
else:
valueToWrite = unsafeAddr valText

when r.colors != NoColors:
let (color, bright) = levelToStyle(r.level)
fgColor(r, color, bright)
append(r.output, key)
resetColors(r)
append(r.output, "=")
fgColor(r, propColor, true)
append(r.output, valueToWrite[])
resetColors(r)

template setFirstProperty*(r: var TextLineRecord, key: string, val: auto) =
setProperty(r, key, val)

proc flushRecord*(r: var TextLineRecord) =
append(r.output, "\n")
flushOutput(r.output)

#
# Textblock records:
#

proc initLogRecord*(r: var TextBlockRecord,
level: LogLevel,
topics: string,
name: string) =
appendHeader(r, level, topics, name, false)
append(r.output, "\n")

proc setProperty*(r: var TextBlockRecord, key: string, val: auto) =
let valText = $val

append(r.output, textBlockIndent)
fgColor(r, propColor, false)
append(r.output, key)
append(r.output, ": ")
applyStyle(r, styleBright)

if valText.find(NewLines) == -1:
append(r.output, valText)
append(r.output, "\n")
else:
let indent = textBlockIndent & repeat(' ', key.len + 2)
var first = true
for line in splitLines(valText):
if not first: append(r.output, indent)
append(r.output, line)
append(r.output, "\n")
first = false

resetColors(r)

template setFirstProperty*(r: var TextBlockRecord, key: string, val: auto) =
setProperty(r, key, val)

proc flushRecord*(r: var TextBlockRecord) =
append(r.output, "\n")
flushOutput(r.output)

#
# JSON records:
#
Expand Down Expand Up @@ -684,6 +516,39 @@ proc flushRecord*(r: var JsonRecord) =

flushOutput r.output

#
# LogRecord functions
#

template initLogRecord*(r: var LogRecord, lvl: LogLevel, topics: string, name: string) =
r.internalStream = memoryOutput()
when r.logFormat == textLines:
r.tlwriter.init(r.internalStream)
r.tlwriter.beginRecord(lvl, topics, name)
elif r.logFormat == textBlocks:
r.tbwriter.init(r.internalStream)
r.tbwriter.beginRecord(lvl, topics, name)

template setProperty*(r: var LogRecord, key: string, val: auto) =
when r.logFormat == textLines:
r.tlwriter.writeField(key, val)
elif r.logFormat == textBlocks:
r.tbwriter.writeField(key, val)

template setFirstProperty*(r: var LogRecord, key: string, val: auto) =
when r.logFormat == textLines:
r.tlwriter.writeField(key, val)
elif r.logFormat == textBlocks:
r.tbwriter.writeField(key, val)

template flushRecord*(r: var LogRecord) =
when r.logFormat == textLines:
r.tlwriter.endRecord()
elif r.logFormat == textBlocks:
r.tbwriter.endRecord()
r.output.append r.internalStream.getOutput(string)
flushOutput r.output

#
# When any of the output streams have multiple output formats, we need to
# create a single tuple holding all of the record types which will be passed
Expand Down
58 changes: 57 additions & 1 deletion chronicles/options.nim
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import
macros, strutils, strformat, sequtils, os
macros, strutils, strformat, sequtils, os, terminal, faststreams/textio

# The default behavior of Chronicles can be configured through a wide-range
# of compile-time -d: switches (for more information, see the README).
Expand Down Expand Up @@ -335,3 +335,59 @@ const
# narrow terminals
# * properies may be easier to find
else: parseSinksSpec "textlines"

#
# color and style support functions
#

const
propColor* = fgBlue
topicsColor* = fgYellow

template levelToStyle*(lvl: LogLevel): untyped =
case lvl
of TRACE: (fgGreen, true)
of DEBUG: (fgGreen, true)
of INFO: (fgGreen, false)
of NOTICE:(fgYellow, false)
of WARN: (fgYellow, true)
of ERROR: (fgRed, false)
of FATAL: (fgRed, true)
of NONE: (fgWhite, false)

template shortName*(lvl: LogLevel): string =
# Same-length strings make for nice alignment
case lvl
of TRACE: "TRC"
of DEBUG: "DBG"
of INFO: "INF"
of NOTICE:"NOT"
of WARN: "WRN"
of ERROR: "ERR"
of FATAL: "FAT"
of NONE: " "

template setForegroundColor*(writer: untyped, color: ForegroundColor, brightness: bool) =
when writer.colorScheme == AnsiColors:
writer.stream.writeText ansiForegroundColorCode(color, brightness)
when writer.colorScheme == NativeColors:
writer.stream.setForegroundColor(color, brightness)

template resetAllColors*(writer: untyped) =
when writer.colorScheme == AnsiColors:
writer.stream.writeText ansiResetCode
when writer.colorScheme == NativeColors:
writer.stream.resetAttributes()

template applyColorStyle*(writer: untyped, style: Style) =
when writer.colorScheme == AnsiColors:
writer.stream.writeText ansiStyleCode(style)
when writer.colorScheme == NativeColors:
writer.stream.setStyle({style})

proc `$`*(ex: ref Exception): string =
result = ""
result &= "exception " & $ex.name & "\n"
result &= "msg \"" & $ex.msg & "\"\n"
when not defined(js) and not defined(nimscript) and hostOS != "standalone":
result &= "location " & getStackTrace(ex).strip
Loading