diff --git a/chronicles.nim b/chronicles.nim index 062bcfb..cd0cbe7 100644 --- a/chronicles.nim +++ b/chronicles.nim @@ -108,7 +108,7 @@ when runtimeFilteringEnabled: proc topicStateIMPL(topicName: static[string]): ptr TopicSettings = # Nim's GC safety analysis gets confused by the global variables here {.gcsafe.}: - var topic {.global.} = TopicSettings(state: Normal, logLevel: NONE) + var topic {.global.} = TopicSettings(state: Normal, logLevel: llNONE) var dummy {.global, used.} = registerTopic(topicName, addr(topic)) return addr(topic) @@ -262,7 +262,7 @@ macro logIMPL(lineInfo: static InstInfo, else: for topic in enabledTopics: if topic.name == t: - if topic.logLevel != NONE: + if topic.logLevel != llNONE: if severity >= topic.logLevel: enabledTopicsMatch = true elif severity >= enabledLogLevel: @@ -270,7 +270,7 @@ macro logIMPL(lineInfo: static InstInfo, if t in requiredTopics: dec requiredTopicsCount - if severity != NONE and not enabledTopicsMatch or requiredTopicsCount > 0: + if severity != llNONE and not enabledTopicsMatch or requiredTopicsCount > 0: return # Handling file name and line numbers on/off (lineNumbersEnabled) for particular log statements @@ -284,7 +284,7 @@ macro logIMPL(lineInfo: static InstInfo, var code = newStmtList() when runtimeFilteringEnabled: - if severity != NONE: + if severity != llNONE: code.add runtimeTopicFilteringCode(severity, activeTopics) # The rest of the code selects the active LogRecord type (which can diff --git a/chronicles.nimble b/chronicles.nimble index c600264..ac5d40d 100644 --- a/chronicles.nimble +++ b/chronicles.nimble @@ -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" diff --git a/chronicles/jsonserializer.nim b/chronicles/jsonserializer.nim new file mode 100644 index 0000000..f066f1f --- /dev/null +++ b/chronicles/jsonserializer.nim @@ -0,0 +1,56 @@ +import + times + +import + faststreams/[outputs, textio], + json_serialization/writer + +import + options + +# TODO: get json_serialization to support Time, ZonedTime, DateTime, etc. + +# Note, cannot inherit from 'JsonWriter' directly because it is not an 'object of RootObj' +# So, we are doing it the hard way with a 'jwriter' field that is an instance of JsonWriter +# type +# CJsonWriter*[timeFormat: static[TimestampsScheme], colorScheme: static[ColorScheme]] = object of JsonWriter + +type + CJsonWriter*[timeFormat: static[TimestampsScheme], colorScheme: static[ColorScheme]] = object + jwriter: JsonWriter + +proc init*(w: var CJsonWriter, stream: OutputStream) = + w.jwriter = JsonWriter.init(stream, pretty = false) + +proc writeFieldName*(w: var CJsonWriter, name: string) = + writeFieldName(w.jwriter, name) + +proc writeValue*(w: var CJsonWriter, value: auto) = + writeValue(w.jwriter, value) + +proc writeArray*[T](w: var CJsonWriter, elements: openarray[T]) = + writeArray(w.jwriter, elements) + +proc writeIterable*(w: var CJsonWriter, collection: auto) = + writeIterable(w.jwriter, collection) + +proc writeField*(w: var CJsonWriter, name: string, value: auto) = + writeField(w.jwriter, name, value) + +proc beginRecord*(w: var CJsonWriter, level: LogLevel, topics, title: string) = + w.jwriter.beginRecord() + if level != llNONE: + w.jwriter.writeField("lvl", level.shortName()) + when w.timeFormat == UnixTime: + w.jwriter.writeField("ts", formatFloat(epochTime(), ffDecimal, 6)) + elif w.timeFormat == RfcTime: + w.jwriter.writeField("ts", now().format("yyyy-MM-dd HH:mm:sszzz")) + w.jwriter.writeField("msg", title) + if topics.len > 0: + w.jwriter.writeField("topics", topics) + +proc endRecord*(w: var CJsonWriter) = + w.jwriter.endRecord() + +proc getStream*(w: var CJsonWriter): OutputStream = + result = w.jwriter.stream diff --git a/chronicles/log_output.nim b/chronicles/log_output.nim index 2df355c..a3dbe4a 100644 --- a/chronicles/log_output.nim +++ b/chronicles/log_output.nim @@ -1,6 +1,11 @@ import strutils, times, macros, options, os, - dynamic_scope_types + dynamic_scope_types, + serialization, + textlineserializer, + textblockserializer + +# TODO: Undo all the damage I did to the "js" scenario when defined(js): import @@ -12,18 +17,18 @@ when defined(js): type OutStr = cstring else: + import - terminal, - faststreams/outputs, json_serialization/writer + faststreams/outputs, + json_serialization/writer + import + jsonserializer export outputs, writer type OutStr = string - const - propColor = fgBlue - topicsColor = fgYellow export LogLevel @@ -56,16 +61,19 @@ 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], + timestamps: static[TimestampsScheme], + colors: static[ColorScheme]] = object output*: Output + # outStream*: OutputStream + case kind: LogFormat + of textLines: + lwriter: TextLineWriter[timestamps, colors] + of textBlocks: + bwriter: TextBlockWriter[timestamps, colors] + of json: + jwriter: CJsonWriter[timestamps, colors] StreamOutputRef*[Stream; outputId: static[int]] = object @@ -74,22 +82,22 @@ type recordType: NimNode outputsTuple: NimNode -when defined(js): - type - JsonRecord*[Output; timestamps: static[TimestampsScheme]] = object - output*: Output - record: js +# when defined(js): +# type +# JsonRecord*[Output; timestamps: static[TimestampsScheme]] = object +# output*: Output +# record: js - JsonString* = distinct string -else: - type - JsonRecord*[Output; timestamps: static[TimestampsScheme]] = object - output*: Output - outStream: OutputStream - jsonWriter: JsonWriter +# JsonString* = distinct string +# else: +# type +# JsonRecord*[Output; timestamps: static[TimestampsScheme]] = object +# output*: Output +# outStream: OutputStream +# jsonWriter: JsonWriter -export - JsonString +# export +# JsonString when defined(posix): {.pragma: syslog_h, importc, header: ""} @@ -253,10 +261,12 @@ 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" + # let RecordType = case sink.format + # of json: bnd"LogRecord" + # of textLines: bnd"LogRecord" + # of textBlocks: bnd"LogRecord" + + let RecordType = bnd"LogRecord" result = newTree(nnkBracketExpr, RecordType) @@ -280,15 +290,15 @@ proc selectRecordType(s: var StreamCodeNodes, sink: SinkSpec): NimNode = else: result.add selectOutputType(s, sink.destinations[0]) + result.add newIdentNode($sink.format) result.add newIdentNode($sink.timestamps) # Set the color scheme for the record types that require it - if sink.format != json: - var colorScheme = sink.colorScheme - when not defined(windows): - # `NativeColors' means `AnsiColors` on non-Windows platforms: - if colorScheme == NativeColors: colorScheme = AnsiColors - result.add newIdentNode($colorScheme) + var colorScheme = sink.colorScheme + when not defined(windows): + # `NativeColors' means `AnsiColors` on non-Windows platforms: + if colorScheme == NativeColors: colorScheme = AnsiColors + result.add newIdentNode($colorScheme) # The `append` and `flushOutput` functions implement the actual writing # to the log destinations (which we call Outputs). @@ -366,12 +376,12 @@ template getOutputStream(o: StreamOutputRef): File = template append*(o: var SysLogOutput, s: OutStr) = let syslogLevel = case o.currentRecordLevel - of TRACE, DEBUG, NONE: LOG_DEBUG - of INFO: LOG_INFO - of NOTICE: LOG_NOTICE - of WARN: LOG_WARNING - of ERROR: LOG_ERR - of FATAL: LOG_CRIT + of TRACE, DEBUG, llNONE: LOG_DEBUG + of INFO: LOG_INFO + of NOTICE: LOG_NOTICE + of WARN: LOG_WARNING + of ERROR: LOG_ERR + of FATAL: LOG_CRIT syslog(syslogLevel or LOG_PID, "%s", s) @@ -455,29 +465,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) @@ -487,201 +474,48 @@ 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: +# LogRecord functions # -template `[]=`(r: var JsonRecord, key: string, val: auto) = - when defined(js): - when val is string: - r.record[key] = when val is string: cstring(val) else: val - else: - r.record[key] = val - else: - writeField(r.jsonWriter, key, val) - -proc initLogRecord*(r: var JsonRecord, - level: LogLevel, - topics: string, - name: string) = - when defined(js): - r.record = newJsObject() - else: - r.outStream = memoryOutput() - r.jsonWriter = JsonWriter.init(r.outStream, pretty = false) - r.jsonWriter.beginRecord() - - if level != NONE: - r["lvl"] = level.shortName - - when r.timestamps != NoTimestamps: - r["ts"] = r.timestamp() - - r["msg"] = name - - if topics.len > 0: - r["topics"] = topics - -proc setProperty*(r: var JsonRecord, key: string, val: auto) = - r[key] = val - -template setFirstProperty*(r: var JsonRecord, key: string, val: auto) = - r[key] = val - -proc flushRecord*(r: var JsonRecord) = - when defined(js): - r.output.append JSON.stringify(r.record) - else: - r.jsonWriter.endRecord() - r.outStream.write '\n' - r.output.append r.outStream.getOutput(string) - +template initLogRecord*(r: var LogRecord, lvl: LogLevel, topics: string, name: string) = + var outStream = memoryOutput().implicitDeref + when r.logFormat == textLines: + r.lwriter.init(outStream) + r.lwriter.beginRecord(lvl, topics, name) + elif r.logFormat == textBlocks: + r.bwriter.init(outStream) + r.bwriter.beginRecord(lvl, topics, name) + elif r.logFormat == json: + r.jwriter.init(outStream) + r.jwriter.beginRecord(lvl, topics, name) + +template setProperty*(r: var LogRecord, key: string, val: auto) = + when r.logFormat == textLines: + r.lwriter.writeField(key, val) + elif r.logFormat == textBlocks: + r.bwriter.writeField(key, val) + elif r.logFormat == json: + r.jwriter.writeField(key, val) + +template setFirstProperty*(r: var LogRecord, key: string, val: auto) = + when r.logFormat == textLines: + r.lwriter.writeField(key, val) + elif r.logFormat == textBlocks: + r.bwriter.writeField(key, val) + elif r.logFormat == json: + r.jwriter.writeField(key, val) + +template flushRecord*(r: var LogRecord) = + when r.logFormat == textLines: + r.lwriter.endRecord() + r.output.append r.lwriter.getStream().getOutput(string) + elif r.logFormat == textBlocks: + r.bwriter.endRecord() + r.output.append r.bwriter.getStream().getOutput(string) + elif r.logFormat == json: + r.jwriter.endRecord() + r.output.append r.jwriter.getStream().getOutput(string) flushOutput r.output # diff --git a/chronicles/options.nim b/chronicles/options.nim index c110834..16ac25a 100644 --- a/chronicles/options.nim +++ b/chronicles/options.nim @@ -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). @@ -37,7 +37,7 @@ when chronicles_enabled_topics.len > 0 and chronicles_required_topics.len > 0: type LogLevel* = enum - NONE, + llNONE, # simple NONE conflicts with https://github.com/status-im/nim-chronos/blob/master/chronos/transports/stream.nim#L39 TRACE, DEBUG, INFO, @@ -97,6 +97,8 @@ type name*: string logLevel*: LogLevel + # WriterType*[LogFormat] = ref object of RootObj + const defaultChroniclesStreamName* = "defaultChroniclesStream" proc handleYesNoOption(optName: string, @@ -151,7 +153,7 @@ proc topicsWithLogLevelAsSeq(topics: string): seq[EnabledTopic] = logLevel: handleEnumOption(LogLevel, values[1]))) else: - sequence.add(EnabledTopic(name: values[0], logLevel: NONE)) + sequence.add(EnabledTopic(name: values[0], logLevel: llNONE)) return sequence proc logFormatFromIdent(n: NimNode): LogFormat = @@ -335,3 +337,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 llNONE: (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 llNONE: " " + +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 diff --git a/chronicles/textblockserializer.nim b/chronicles/textblockserializer.nim new file mode 100644 index 0000000..ab74e8f --- /dev/null +++ b/chronicles/textblockserializer.nim @@ -0,0 +1,140 @@ +import + times, + strutils, + terminal, + system, + options + +import + serialization, + faststreams/[outputs, textio] + +import + chronicles, + options + +type + TextBlockWriter*[timeFormat: static[TimestampsScheme], colorScheme: static[ColorScheme]] = object + stream: OutputStream + currentLevel: LogLevel + + TextBlockReader* = object + lexer: string + +serializationFormat TextBlock, + Reader = TextBlockReader, + Writer = TextBlockWriter, + PreferedOutput = string, + mimeType = "text/plain" + +# +# Class startup +# + +proc init*(w: var TextBlockWriter, stream: OutputStream) = + w.stream = stream + +# +# Field Handling +# + +proc writeValue*(w: var TextBlockWriter, value: auto, prefix = 0) # forward ref + +proc writeFieldName*(w: var TextBlockWriter, name: string) = + setForegroundColor(w, propColor, false) + w.stream.writeText name + w.stream.writeText ": " + resetAllColors(w) + +proc writeArray*[T](w: var TextBlockWriter, elements: openarray[T]) = + w.stream.writeText '[' + let clen = elements.len - 1 + for index, value in elements.pairs: + w.stream.writeText value + if index < clen: + w.stream.writeText ", " + w.stream.writeText ']' + +proc writeIterable*(w: var TextBlockWriter, collection: auto) = + w.stream.writeText '[' + let clen = collection.len - 1 + for index, value in collection.pairs: + w.stream.writeText value + if index < clen: + w.stream.writeText ", " + w.stream.writeText ']' + +proc writeField*(w: var TextBlockWriter, name: string, value: auto, prefix = 0) = + w.stream.writeText textBlockIndent & repeat(' ', prefix) + writeFieldName(w, name) + writeValue(w, value, prefix=name.len + 2) + +type + SomeTime = Time | DateTime | times.Duration | TimeInterval | Timezone | ZonedTime + +proc writeValue*(w: var TextBlockWriter, value: auto, prefix = 0) = + setForegroundColor(w, propColor, false) + applyColorStyle(w, styleBright) + when value is array or value is seq: + writeIterable(w, value) + w.stream.writeText "\n" + elif value is SomeTime | SomeNumber | bool: # all types that sit on a single line + w.stream.writeText value + w.stream.writeText "\n" + elif value is object: + w.stream.write $type(value) + w.stream.write "{\n" + resetAllColors(w) + enumInstanceSerializedFields(value, fieldName, fieldValue): + writeField(w, fieldName, fieldValue, prefix=prefix + textBlockIndent.len) + w.stream.writeText textBlockIndent & repeat(' ', prefix) + setForegroundColor(w, propColor, false) + applyColorStyle(w, styleBright) + w.stream.write "}\n" + else: + var first = true + for part in ($value).splitLines: + if not first: + w.stream.writeText textBlockIndent & repeat(' ', prefix) + w.stream.writeText part + w.stream.writeText "\n" + first = false + resetAllColors(w) + +# template endRecordField*(w: var TextBlockWriter) = +# discard + +# +# Record Handling +# + +proc beginRecord*(w: var TextBlockWriter, level: LogLevel, topics, title: string) = + w.currentLevel = level + let (logColor, logBright) = levelToStyle(level) + setForegroundColor(w, logColor, logBright) + w.stream.writeText shortName(w.currentLevel) + resetAllColors(w) + when w.timeFormat == UnixTime: + w.stream.writeText ' ' + w.stream.writeText formatFloat(epochTime(), ffDecimal, 6) + when w.timeFormat == RfcTime: + w.stream.writeText now().format(" yyyy-MM-dd HH:mm:sszzz") + let titleLen = title.len + if titleLen > 0: + w.stream.writeText ' ' + applyColorStyle(w, styleBright) + w.stream.writetext title + resetAllColors(w) + if topics.len > 0: + w.stream.writeText " topics=\"" + setForegroundColor(w, topicsColor, true) + w.stream.writeText topics + resetAllColors(w) + w.stream.writeText '"' + w.stream.writeText '\n' + +proc endRecord*(w: var TextBlockWriter) = + w.stream.writeText '\n' + +proc getStream*(w: var TextBlockWriter): OutputStream = + result = w.stream diff --git a/chronicles/textlineserializer.nim b/chronicles/textlineserializer.nim new file mode 100644 index 0000000..a1af3a9 --- /dev/null +++ b/chronicles/textlineserializer.nim @@ -0,0 +1,149 @@ +import + times, + strutils, + terminal, + system, + options + +import + serialization, + faststreams/[outputs, textio] + +import + chronicles, + options + +type + TextLineWriter*[timeFormat: static[TimestampsScheme], colorScheme: static[ColorScheme]] = object + stream: OutputStream + currentLevel: LogLevel + + TextLineReader* = object + lexer: string + +serializationFormat TextLine, + Reader = TextLineReader, + Writer = TextLineWriter, + PreferedOutput = string, + mimeType = "text/plain" + +# +# value support functions +# + +const + escChars: set[char] = strutils.NewLines + {'"', '\\'} + quoteChars: set[char] = {' ', '='} + +proc quoteIfNeeded(w: var TextLineWriter, val: SomeOrdinal) = + w.stream.writeText val + +proc quoteIfNeeded(w: var TextLineWriter, val: auto) = + let valText = $val + let + needsEscape = valText.find(escChars) > -1 + needsQuote = (valText.find(quoteChars) > -1) or needsEscape + if needsQuote: + var quoted = "" + quoted.addQuoted valText + w.stream.writeText quoted + else: + w.stream.writeText val + +proc quoteIfNeeded(w: var TextLineWriter, val: ref Exception) = + w.stream.writeText val.name + w.stream.writeText '(' + w.quoteIfNeeded val.msg + when not defined(js) and not defined(nimscript) and hostOS != "standalone": + w.stream.writeText ", " + w.quoteIfNeeded getStackTrace(val).strip + w.stream.writeText ')' + +# +# Class startup +# + +proc init*(w: var TextLineWriter, stream: OutputStream) = + w.stream = stream + +# +# Field Handling +# + +proc writeFieldName*(w: var TextLineWriter, name: string) = + w.stream.writeText ' ' + let (color, bright) = levelToStyle(w.currentLevel) + setForegroundColor(w, color, bright) + w.stream.writeText name + resetAllColors(w) + w.stream.writeText "=" + +proc writeValue*(w: var TextLineWriter, value: auto) = + setForegroundColor(w, propColor, true) + w.quoteIfNeeded(value) + resetAllColors(w) + +proc writeArray*[T](w: var TextLineWriter, elements: openarray[T]) = + w.stream.writeText '[' + let clen = elements.len - 1 + for index, value in elements.pairs: + w.stream.writeText value + if index < clen: + w.stream.writeText ", " + w.stream.writeText ']' + +proc writeIterable*(w: var TextLineWriter, collection: auto) = + w.stream.writeText '[' + let clen = collection.len - 1 + for index, value in collection.pairs: + w.stream.writeText value + if index < clen: + w.stream.writeText ", " + w.stream.writeText ']' + +proc writeField*(w: var TextLineWriter, name: string, value: auto) = + writeFieldName(w, name) + writeValue(w, value) + +# template endRecordField*(w: var TextLineWriter) = +# discard + +# +# Record Handling +# +proc beginRecord*(w: var TextLineWriter, level: LogLevel, topics, title: string) = + w.currentLevel = level + let (logColor, logBright) = levelToStyle(level) + setForegroundColor(w, logColor, logBright) + w.stream.writeText shortName(w.currentLevel) + resetAllColors(w) + when w.timeFormat == UnixTime: + w.stream.writeText ' ' + w.stream.writeText formatFloat(epochTime(), ffDecimal, 6) + when w.timeFormat == RfcTime: + w.stream.writeText now().format(" yyyy-MM-dd HH:mm:sszzz") + let titleLen = title.len + if titleLen > 0: + w.stream.writeText ' ' + applyColorStyle(w, styleBright) + if titleLen > 42: + w.stream.writetext title + else: + for index in 0 ..< 42: + if index < titleLen: + w.stream.writeText title[index] + else: + w.stream.writeText ' ' + resetAllColors(w) + if topics.len > 0: + w.stream.writeText " topics=\"" + setForegroundColor(w, topicsColor, true) + w.stream.writeText topics + resetAllColors(w) + w.stream.writeText '"' + +proc endRecord*(w: var TextLineWriter) = + w.stream.write '\n' + +proc getStream*(w: var TextLineWriter): OutputStream = + result = w.stream